<!-- Copyright Epic Games, Inc. All Rights Reserved. -->
<html>
<title>GPU Dump Viewer</title>

<script type="text/javascript">


// -------------------------------------------------------------------- CONSTANTS

const k_current_dir = "./";

const k_style_scroll_width = 8;
const k_style_padding_size = 10;
const k_style_main_div_margin = 15;

const k_null_json_ptr = "0000000000000000";


// -------------------------------------------------------------------- GLOBALS

var g_dump_service = {};
var g_infos = {};
var g_dump_cvars = {};
var g_passes = [];
var g_descs = {};

var g_view = null;


// -------------------------------------------------------------------- GLOBALS

class IView
{
	constructor()
	{

	}

	setup_html(parent)
	{
		parent.innerHTML = '';
	}

	resize(ctx)
	{

	}

	get navigations()
	{
		return [];
	}

	release()
	{

	}
}

function set_main_view(new_view)
{
	var parent_dom = document.getElementById('main_right_pannel');
	if (g_view !== null)
	{
		g_view.release();
		delete g_view;
		parent_dom.innerHTML = '';
	}
	g_view = new_view;
	if (g_view !== null)
	{
		g_view.setup_html(parent_dom);
		onresize_body();
	}
}


// -------------------------------------------------------------------- FILE LOADING

function does_file_exists(relative_path)
{
	try
	{
		var request = new XMLHttpRequest();
		request.open('HEAD', k_current_dir + relative_path, false);
		request.send(null);
		return request.status != 404;
	}
	catch (error)
	{
		return false;
	}
	return false;
}

function load_text_file(relative_path)
{
	try
	{
		var request = new XMLHttpRequest();
		request.open('GET', k_current_dir + relative_path, false);
		request.send(null);
		if (request.status === 0 || request.status === 200)
		{
			return request.responseText;
		}
		add_console_event('error', `couldn't load ${relative_path}: Returned HTTP status ${request.status}`);
	}
	catch (error)
	{
		add_console_event('error', `couldn't load ${relative_path}: ${error}`);
	}
	return null;
}

function load_binary_file(relative_path, callback)
{
	try
	{
		var request = new XMLHttpRequest();
		request.open('GET', k_current_dir + relative_path, true);
		request.responseType = "arraybuffer";

		request.onload = function(event)
		{
			var array_buffer = request.response;
			if ((request.status === 0 || request.status === 200) && array_buffer)
			{
				callback(array_buffer);
			}
			else
			{
				add_console_event('error', `couldn't load ${relative_path}: Returned HTTP status ${request.status}`);
				callback(null);
			}
		};

		request.onerror = function() {
			add_console_event('error', `couldn't load ${relative_path}`);
			callback(null);
		};

		request.send(null);
	}
	catch (error)
	{
		add_console_event('error', `couldn't load ${relative_path}: ${error}`);
		callback(null);
	}
}

function load_resource_binary_file(relative_path, callback)
{
	return load_binary_file(relative_path, function(raw_texture_data)
	{
		var compression_type = '';
		if (g_dump_service['CompressionName'] == 'Zlib')
		{
			compression_type = 'deflate';
		}
		else if (g_dump_service['CompressionName'] == 'GZip')
		{
			compression_type = 'gzip';
		}

		if (raw_texture_data === null || compression_type == '')
		{
			return callback(raw_texture_data);
		}

		var decompressor = new DecompressionStream(compression_type);
		var decompressed_stream = new Blob([raw_texture_data]).stream().pipeThrough(decompressor);
		new Response(decompressed_stream).arrayBuffer().then(callback, function() { callback(null); });
	});
}

function load_json(relative_path)
{
	var txt = load_text_file(relative_path);

	if (txt === null)
	{
		return null;
	}

	return JSON.parse(load_text_file(relative_path));
}

function load_json_dict_sequence(relative_path)
{
	var text_file = load_text_file(relative_path);
	if (text_file == '')
	{
		return new Array();
	}
	var dicts = text_file.split("}{");
	var dict_sequence = [];
	dicts.forEach(function(value, index, array) {
		if (!value.startsWith('{'))
		{
			value = '{' + value;
		}
		if (!value.endsWith('}'))
		{
			value = value + '}';
		}
		dict_sequence.push(JSON.parse(value));
	});
	return dict_sequence;
}

function get_resource_desc(unique_resource_name)
{
	return g_descs[unique_resource_name];
}

function load_structure_metadata(structure_ptr)
{
	var cache = {};

	function load_nested_structure_metadata(nested_structure_ptr)
	{
		if (nested_structure_ptr in cache)
		{
			return cache[nested_structure_ptr];
		}

		var metadata = load_json(`StructuresMetadata/${nested_structure_ptr}.json`);
		cache[nested_structure_ptr] = metadata;
 
		for (var member of metadata['Members'])
		{
			if (member['StructMetadata'] == k_null_json_ptr)
			{
				member['StructMetadata'] = null;
			}
			else
			{
				member['StructMetadata'] = load_nested_structure_metadata(member['StructMetadata']);
			}
		}

		return metadata;
	}

	return load_nested_structure_metadata(structure_ptr);
}


// -------------------------------------------------------------------- UTILITY

function get_filename(file_path)
{
	return file_path.split(/(\\|\/)/g).pop();
}

function px_string_to_int(str)
{
	if (Number.isInteger(str))
	{
		return str;
	}
	if (str.endsWith('px'))
	{
		return Number(str.substring(0, str.length - 2));
	}
	return Number(str);
}

function parse_subresource_unique_name(subresource_unique_name)
{
	var splitted_name = subresource_unique_name.split(".");

	var subresource_info = {};
	subresource_info['subresource'] = null;
	subresource_info['array_slice'] = null;

	if (splitted_name[splitted_name.length - 1].startsWith('mip') || splitted_name[splitted_name.length - 1] == 'stencil')
	{
		subresource_info['subresource'] = splitted_name.pop();
	}

	if (splitted_name[splitted_name.length - 1].startsWith('[') && splitted_name[splitted_name.length - 1].endsWith(']'))
	{
		var array_slice_bracket = splitted_name.pop();
		subresource_info['array_slice'] = parseInt(array_slice_bracket.substring(1, array_slice_bracket.length - 1));
	}

	subresource_info['resource'] = splitted_name.join('.');

	return subresource_info;
}

function get_subresource_unique_name(subresource_info)
{
	var subresource_unique_name = subresource_info['resource'];
	if (subresource_info['array_slice'] !== null)
	{
		subresource_unique_name += `.[${subresource_info['array_slice']}]`;
	}
	if (subresource_info['subresource'] !== null)
	{
		subresource_unique_name += `.${subresource_info['subresource']}`;
	}
	return subresource_unique_name;
}

function prettify_subresource_unique_name(subresource_info, resource_desc)
{
	if (!resource_desc)
	{
		return get_subresource_unique_name(subresource_info);
	}

	var name = resource_desc['Name'];

	if (subresource_info['array_slice'] !== null)
	{
		name += ` slice[${subresource_info['array_slice']}]`;
	}

	if (subresource_info['subresource'] !== null && (subresource_info['subresource'] == 'stencil' || resource_desc['NumMips'] > 1))
	{
		name += ` ${subresource_info['subresource']}`;
	}
	
	return name;
}

function parse_subresource_unique_version_name(subresource_unique_version_name)
{
	var splitted_name = subresource_unique_version_name.split(".");

	var pass_ptr = -1;
	var draw_id = -1;

	var last = splitted_name.pop();
	if (last.startsWith('d'))
	{
		draw_id = parseInt(last.substring(1));
		last = splitted_name.pop();
	}
	
	if (last.startsWith('v'))
	{
		pass_ptr = last.substring(1);
	}

	var subresource_unique_name = splitted_name.join('.');
	var subresource_version_info = parse_subresource_unique_name(subresource_unique_name);

	subresource_version_info['pass'] = pass_ptr;
	subresource_version_info['draw'] = draw_id;
	return subresource_version_info;
}

function get_subresource_unique_version_name(subresource_version_info)
{
	if (subresource_version_info['draw'] >= 0)
	{
		return `${get_subresource_unique_name(subresource_version_info)}.v${subresource_version_info['pass']}.d${subresource_version_info['draw']}`;
	}
	return `${get_subresource_unique_name(subresource_version_info)}.v${subresource_version_info['pass']}`;
}


// -------------------------------------------------------------------- SHADER PARAMETERS

function iterate_structure_members(root_structure_metadata, callback)
{
	function iterate_recursive(structure_metadata, offset, cpp_prefix, shader_prefix)
	{
		for (var member of structure_metadata['Members'])
		{
			var base_type = member['BaseType'];

			if (base_type == 'UBMT_NESTED_STRUCT')
			{
				iterate_recursive(member['StructMetadata'], offset + member['Offset'], cpp_prefix + member['Name'] + '.', shader_prefix + member['Name'] + '_');
			}
			else if (base_type == 'UBMT_INCLUDED_STRUCT')
			{
				iterate_recursive(member['StructMetadata'], offset + member['Offset'], cpp_prefix + member['Name'] + '.', shader_prefix);
			}
			else
			{
				var params = {
					member: member,
					base_type: base_type,
					byte_offset: offset + member['Offset'],
					cpp_name: cpp_prefix + member['Name'],
					shader_name: shader_prefix + member['Name'],
				};
				callback(params);
			}
		}
	}

	iterate_recursive(root_structure_metadata, /* offset = */ 0, /* cpp_prefix = */ '', /* shader_prefix = */ '');
}


// -------------------------------------------------------------------- FLOAT ENCODING

function decode_float(raw, total_bit_count, exp_bit_count, has_sign)
{
	var exp_bias = (1 << (exp_bit_count - 1)) - 1;
	var mantissa_bit_count = total_bit_count - exp_bit_count - (has_sign ? 1 : 0);

	var sign_bit      = (raw >> (total_bit_count - 1)) & 0x1;
	var mantissa_bits = (raw >> 0)                     & ((0x1 << mantissa_bit_count) - 1);
	var exp_bits      = (raw >> mantissa_bit_count)    & ((0x1 << exp_bit_count) - 1);

	var is_max_exp = exp_bits == ((0x1 << exp_bit_count) - 1);

	var is_denormal = exp_bits == 0;
	var is_infinity = is_max_exp && mantissa_bits == 0;
	var is_nan = is_max_exp && mantissa_bits != 0;

	var exp = exp_bits - exp_bias;
	var mantissa = mantissa_bits * Math.pow(0.5, mantissa_bit_count);
	var sign = (has_sign && (sign_bit == 1)) ? -1 : 1;

	if (is_nan)
	{
		return 'nan';
	}
	else if (is_infinity)
	{
		return sign == -1 ? '-inf' : '+inf';
	}
	else if (is_denormal)
	{
		var value = sign * mantissa * Math.pow(0.5, exp_bias - 1);
		return value;
	}
	else
	{
		var value = sign * (1.0 + mantissa) * Math.pow(2.0, exp);
		return value;
	}
}

function decode_float10(raw)
{
	return decode_float(raw, /* total_bit_count = */ 10, /* exp_bit_count = */ 5, /* has_sign = */ false);
}

function decode_float11(raw)
{
	return decode_float(raw, /* total_bit_count = */ 11, /* exp_bit_count = */ 5, /* has_sign = */ false);
}

function decode_float16(raw)
{
	return decode_float(raw, /* total_bit_count = */ 16, /* exp_bit_count = */ 5, /* has_sign = */ true);
}

function decode_float32(raw)
{
	return decode_float(raw, /* total_bit_count = */ 32, /* exp_bit_count = */ 8, /* has_sign = */ true);
}

function test_decode_float()
{
	var tests = [
		// Zero
		[0x0000,  0.0],
		[0x8000, -0.0],

		// normals
		[0x4000,  2.0],
		[0xc000, -2.0],

		// denormals
		[0x0001,  5.960464477539063e-8],
		[0x8001, -5.960464477539063e-8],

		// exotics
		[0x7c00, '+inf'],
		[0xFc00, '-inf'],
		[0x7c01, 'nan'],
		[0xFc01, 'nan'],
	];

	for (var i = 0; i < tests.length; i++)
	{
		var encoded = tests[i][0];
		var ref = tests[i][1];
		var computed = decode_float16(encoded);

		console.assert(computed == ref);
	}
}


// -------------------------------------------------------------------- PASS ANALYSIS

var g_analysis = {};

function get_frame_number(pass_data)
{
	var pass_event_scopes = pass_data['ParentEventScopes'];
	var frame_event_scope_name = pass_event_scopes[pass_event_scopes.length - 1];

	const regex = new RegExp("Frame (?<FrameNumber>\\d+).*");
	//const regex = /Frame (?<FrameNumber>\d+) .*"/;

	var found = frame_event_scope_name.match(regex);
	return Number(found.groups['FrameNumber']);
}

function compare_draw_events(ref_draw_event, tested_draw_event)
{
	ref_draw_event = ref_draw_event.replace(/\d+\.d*/g, '');
	ref_draw_event = ref_draw_event.replace(/\d+/g, '');

	tested_draw_event = tested_draw_event.replace(/\d+\.d*/g, '');
	tested_draw_event = tested_draw_event.replace(/\d+/g, '');

	return ref_draw_event == tested_draw_event;
}

function is_pass_output_resource(pass_data, subresource_version_info)
{
	var subresource_unique_name = get_subresource_unique_name(subresource_version_info);

	var is_output_resource = null;
	if (pass_data['InputResources'].includes(subresource_unique_name))
	{
		is_output_resource = false;
	}
	else if (pass_data['OutputResources'].includes(subresource_unique_name))
	{
		is_output_resource = true;
	}
	console.assert(is_output_resource !== null);
	return is_output_resource;
}

function find_similar_pass_id_in_frame(current_frame_pass_data, new_frame_id)
{
	var current_pass_event_scopes = current_frame_pass_data['ParentEventScopes'];

	for (var pass_id in g_passes)
	{
		var pass_data = g_passes[pass_id];
		if (!compare_draw_events(pass_data['EventName'], current_frame_pass_data['EventName']))
		{
			continue;
		}

		var frame_id = get_frame_number(pass_data);
		if (frame_id != new_frame_id)
		{
			continue;
		}

		var pass_event_scopes = pass_data['ParentEventScopes'];
		if (pass_event_scopes.length != current_pass_event_scopes.length)
		{
			continue;
		}
		else
		{
			var is_same_parent = true;
			for (var parent_scope_id = 0; parent_scope_id < pass_event_scopes.length - 1; parent_scope_id++)
			{
				is_same_parent = is_same_parent && compare_draw_events(current_pass_event_scopes[parent_scope_id], pass_event_scopes[parent_scope_id]);
			}
			if (!is_same_parent)
			{
				continue;
			}
		}

		return Number(pass_id);
	}

	return null;
}

function find_similar_resource_in_pass(current_frame_pass_data, current_frame_resource_desc, current_frame_subresource_info, new_pass_data)
{
	var is_output_resource = is_pass_output_resource(current_frame_pass_data, current_frame_subresource_info);

	var new_pass_resource_list = is_output_resource ? new_pass_data['OutputResources'] : new_pass_data['InputResources'];

	for (var new_pass_subresource_unique_name of new_pass_resource_list)
	{
		var new_pass_subresource_info = parse_subresource_unique_name(new_pass_subresource_unique_name);
		if (new_pass_subresource_info['subresource'] !== current_frame_subresource_info['subresource'])
		{
			continue;
		}
		if (new_pass_subresource_info['array_slice'] !== current_frame_subresource_info['array_slice'])
		{
			continue;
		}

		var new_pass_resource_desc = get_resource_desc(new_pass_subresource_info['resource']);
		if (new_pass_resource_desc['Name'] !== current_frame_resource_desc['Name'])
		{
			continue;
		}

		return new_pass_subresource_info;
	}

	// try to find with non matching array_slice
	for (var new_pass_subresource_unique_name of new_pass_resource_list)
	{
		var new_pass_subresource_info = parse_subresource_unique_name(new_pass_subresource_unique_name);
		if (new_pass_subresource_info['subresource'] !== current_frame_subresource_info['subresource'])
		{
			continue;
		}

		var new_pass_resource_desc = get_resource_desc(new_pass_subresource_info['resource']);
		if (new_pass_resource_desc['Name'] !== current_frame_resource_desc['Name'])
		{
			continue;
		}

		return new_pass_subresource_info;
	}


	return null;
}

function analyses_passes()
{
	var frames = [];

	for (var pass_id in g_passes)
	{
		var pass_data = g_passes[pass_id];
		
		var frame_id = get_frame_number(pass_data);

		if (!frames.includes(frame_id))
		{
			frames.push(frame_id);
		}
	}

	g_analysis['FrameList'] = frames;
}


// -------------------------------------------------------------------- DISPLAY PASS

function display_pass_hierarchy()
{
	var search_pass = document.getElementById('pass_search_input').value;
	var search_resource = document.getElementById('resource_search_input').value;
	var html = '';
	var parent_event_scopes = [];

	g_passes.forEach(function(pass_data, pass_id) {
		var pass_event_scopes = pass_data['ParentEventScopes'];

		var show_pass = true;
		if (search_pass != '')
		{
			show_pass = pass_data['EventName'].toLowerCase().includes(search_pass.toLowerCase());

			for (var i = 0; i < pass_event_scopes.length; i++)
			{
				show_pass = show_pass || pass_event_scopes[i].toLowerCase().includes(search_pass.toLowerCase());
			}
		}

		var show_resource = true;
		if (search_resource != '')
		{
			show_resource = false;
			for (var subresource_unique_name of pass_data['InputResources'])
			{
				var resource_unique_name = parse_subresource_unique_name(subresource_unique_name)['resource'];
				var resource_name = get_resource_desc(resource_unique_name)['Name'];
				show_resource = show_resource || resource_name.toLowerCase().includes(search_resource.toLowerCase());
			}
			for (var subresource_unique_name of pass_data['OutputResources'])
			{
				var resource_unique_name = parse_subresource_unique_name(subresource_unique_name)['resource'];
				var resource_name = get_resource_desc(resource_unique_name)['Name'];
				show_resource = show_resource || resource_name.toLowerCase().includes(search_resource.toLowerCase());
			}
		}

		var has_input_or_outputs = pass_data['InputResources'].length > 0 || pass_data['OutputResources'].length > 0;

		if (show_pass && show_resource && has_input_or_outputs)
		{
			var shared_scope = 0;
			for (var i = 0; i < Math.min(parent_event_scopes.length, pass_event_scopes.length); i++)
			{
				if (parent_event_scopes[i] == pass_event_scopes[pass_event_scopes.length - 1 - i])
				{
					shared_scope++;
				}
				else
				{
					break;
				}
			}

			parent_event_scopes = parent_event_scopes.slice(0, shared_scope);

			for (var i = shared_scope; i < pass_event_scopes.length; i++)
			{
				var scope = pass_event_scopes[pass_event_scopes.length - 1 - i];
				html += `<a style="padding-left: ${10 + 16 * i}px;" class="disabled">${scope}</a>`;
				parent_event_scopes.push(scope);
			}

			html += `<a style="padding-left: ${10 + 16 * pass_event_scopes.length}px;" href="#display_pass(${pass_id});">${pass_data['EventName']}</a>`;
		}
	});
	document.getElementById('pass_hierarchy').innerHTML = html;
	update_href_selection(document.getElementById('pass_hierarchy'));
}

class ResourceView extends IView
{
	constructor(subresource_version_info, resource_desc)
	{
		super();
		this.subresource_version_info = subresource_version_info;
		this.resource_desc = resource_desc;
		this.onload = function() { };
	}

	get navigations()
	{
		return [];
	}
}

class PassView extends IView
{
	constructor(pass_id)
	{
		super();
		this.pass_id = pass_id;
		this.pass_data = g_passes[pass_id];
		this.pass_draws_data = [];
		this.resource_view = null;

		if (this.pass_data['DrawCount'] > 0)
		{
			this.pass_draws_data = load_json_dict_sequence(`Passes/Pass.${this.pass_data['Pointer']}.Draws.json`);
		}
	}

	setup_html(parent_dom)
	{
		var column_width = '50%';
		var draw_column_display = 'none';

		if (this.pass_draws_data.length > 0)
		{
			column_width = '33%';
			draw_column_display = 'block';
		}

		parent_dom.innerHTML = `
			<div class="pass_title main_div">
				<div id="pass_frame_list" style="display:none;"></div>
				${this.pass_data['EventName']}
				<div class="button_list" style="display:inline-block;"><a href="#display_pass_parameters(${this.pass_id});">PassParameters</a></div>
			</div>
			<table width="100%">
				<tr>
					<td width="${column_width}">
						<div class="main_div">
							<div class="selection_list_title">Input resources</div>
							<div class="selection_list_search">
								<input type="search" id="pass_input_resource_search" oninput="g_view.refresh_input_resource_list();" onchange="g_view.refresh_input_resource_list();" placeholder="Search input resource..." />
							</div>
							<div class="selection_list" id="pass_input_resource_list"></div>
						</div>
					</td>
					<td width="${column_width}">
						<div class="main_div">
							<div class="selection_list_title">Output resources</div>
							<div class="selection_list_search">
								<input type="search" id="pass_output_resource_search" oninput="g_view.refresh_output_resource_list();" onchange="g_view.refresh_output_resource_list();" placeholder="Search output resource..." />
							</div>
							<div class="selection_list" id="pass_output_resource_list"></div>
						</div>
					</td>
					<td width="${column_width}">
						<div class="main_div" style="display: ${draw_column_display};">
							<div class="selection_list_title">Draws</div>
							<div class="selection_list_search">
								<input type="search" id="pass_draw_search" oninput="g_view.refresh_draw_resource_list();" onchange="g_view.refresh_draw_resource_list();" placeholder="Search draw..." />
							</div>
							<div class="selection_list" id="pass_draw_list"></div>
						</div>
					</td>
				</tr>
			</table>
			<div id="display_resource_pannel"></div>
			<table width="100%">
				<tr>
					<td width="50%">
						<div class="main_div" id="resource_pass_modifying_outter">
							<div class="selection_list_title" id="resource_pass_modifying_title"></div>
							<div class="selection_list_search">
								<input type="search" id="resource_pass_modifying_search" oninput="g_view.refresh_resource_modifying_list();" onchange="g_view.refresh_resource_modifying_list();" placeholder="Search modifying pass..." />
							</div>
							<div class="selection_list" id="resource_pass_modifying_list"></div>
						</div>
					</td>
					<td width="50%">
						<div class="main_div" id="resource_pass_reading_outter">
							<div class="selection_list_title" id="resource_pass_reading_title"></div>
							<div class="selection_list_search">
								<input type="search" id="resource_pass_reading_search" oninput="g_view.refresh_resource_reading_list();" onchange="g_view.refresh_resource_reading_list();" placeholder="Search reading pass..." />
							</div>
							<div class="selection_list" id="resource_pass_reading_list"></div>
						</div>
					</td>
				</tr>
			</table>
			`;
	}

	refresh_all_lists()
	{
		this.refresh_frames_lists();
		this.refresh_input_resource_list();
		this.refresh_output_resource_list();
		this.refresh_draw_resource_list();
		this.refresh_resource_modifying_list();
		this.refresh_resource_reading_list();
	}

	refresh_frames_lists()
	{
		if (g_analysis['FrameList'].length == 1)
		{
			return;
		}

		if (this.resource_view === null)
		{
			document.getElementById('pass_frame_list').style.display = 'none';
			return;
		}

		var pass_data = this.pass_data;
		var subresource_version_info = this.resource_view.subresource_version_info;

		var current_frame_number = get_frame_number(this.pass_data);
		var prev_frame_number = g_analysis['FrameList'].includes(current_frame_number - 1) ? current_frame_number - 1 : g_analysis['FrameList'][g_analysis['FrameList'].length - 1];
		var next_frame_number = g_analysis['FrameList'].includes(current_frame_number + 1) ? current_frame_number + 1 : g_analysis['FrameList'][0];

		var prev_frame_pass_id = find_similar_pass_id_in_frame(this.pass_data, prev_frame_number);
		var next_frame_pass_id = find_similar_pass_id_in_frame(this.pass_data, next_frame_number);

		if (prev_frame_pass_id == null || next_frame_pass_id == null)
		{
			document.getElementById('pass_frame_list').style.display = 'none';
			return;
		}

		var prev_frame_href = '';
		var next_frame_href = '';
		if (this.resource_view.resource_desc['Desc'] == 'FRDGParameterStruct')
		{
			prev_frame_href = `#display_pass_parameters(${prev_frame_pass_id});`;
			next_frame_href = `#display_pass_parameters(${next_frame_pass_id});`;
		}
		else
		{
			var prev_frame_pass_data = g_passes[prev_frame_pass_id];
			var next_frame_pass_data = g_passes[next_frame_pass_id];

			var is_output_resource = is_pass_output_resource(this.pass_data, this.resource_view.subresource_version_info);
			var prev_frame_subresource_info = find_similar_resource_in_pass(this.pass_data, this.resource_view.resource_desc, this.resource_view.subresource_version_info, prev_frame_pass_data);
			var next_frame_subresource_info = find_similar_resource_in_pass(this.pass_data, this.resource_view.resource_desc, this.resource_view.subresource_version_info, next_frame_pass_data);

			var prev_frame_href = '';
			var next_frame_href = '';
			if (prev_frame_subresource_info == null || next_frame_subresource_info == null)
			{
				document.getElementById('pass_frame_list').style.display = 'none';
				return;
			}
			else if (is_output_resource)
			{
				prev_frame_href = `#display_output_resource(${prev_frame_pass_id},'${get_subresource_unique_name(prev_frame_subresource_info)}');`;
				next_frame_href = `#display_output_resource(${next_frame_pass_id},'${get_subresource_unique_name(next_frame_subresource_info)}');`;
			}
			else
			{
				prev_frame_href = `#display_input_resource(${prev_frame_pass_id},'${get_subresource_unique_name(prev_frame_subresource_info)}');`;
				next_frame_href = `#display_input_resource(${next_frame_pass_id},'${get_subresource_unique_name(next_frame_subresource_info)}');`;
			}
		}

		document.getElementById('pass_frame_list').style.display = 'inline-block';
		document.getElementById('pass_frame_list').innerHTML = `
			<div class="button_list" style="display:inline-block;"><a href="${prev_frame_href}">-</a><a title="Id of the current frame">${current_frame_number}</a><a href="${next_frame_href}">+</a></div>
			`;
	}

	refresh_input_resource_list()
	{
		var display_list = [];
		for (const subresource_unique_name of this.pass_data['InputResources'])
		{
			var subresource_info = parse_subresource_unique_name(subresource_unique_name);
			var resource_desc = get_resource_desc(subresource_info['resource']);

			var name = prettify_subresource_unique_name(subresource_info, resource_desc);
			var href = null;
			if (resource_desc)
			{
				href = `#display_input_resource(${this.pass_id},'${subresource_unique_name}');`
			}

			display_list.push({'name': name, 'href': href});
		}

		render_selection_list_html(
			document.getElementById('pass_input_resource_list'),
			display_list,
			{
				'search': document.getElementById('pass_input_resource_search').value,
				'deduplicate': true,
				'sort': true
			});
	}

	refresh_output_resource_list()
	{
		var draw_id = -1;
		if (this.resource_view)
		{
			draw_id = this.resource_view.subresource_version_info['draw'];
		}

		var display_list = [];
		for (const subresource_unique_name of this.pass_data['OutputResources'])
		{
			var subresource_info = parse_subresource_unique_name(subresource_unique_name);
			var resource_desc = get_resource_desc(subresource_info['resource']);

			var name = prettify_subresource_unique_name(subresource_info, resource_desc);
			var href = null;
			if (resource_desc)
			{
				if (draw_id >= 0)
				{
					href = `#display_draw_output_resource(${this.pass_id},'${subresource_unique_name}',${draw_id});`;
				}
				else
				{
					href = `#display_output_resource(${this.pass_id},'${subresource_unique_name}');`;
				}
			}

			display_list.push({'name': name, 'href': href});
		}

		render_selection_list_html(
			document.getElementById('pass_output_resource_list'),
			display_list,
			{
				'search': document.getElementById('pass_output_resource_search').value,
			});
	}

	refresh_draw_resource_list()
	{
		var is_output_resource = false;
		var subresource_unique_name = '';
		if (this.resource_view)
		{
			is_output_resource = is_pass_output_resource(this.pass_data, this.resource_view.subresource_version_info);
			subresource_unique_name = get_subresource_unique_name(this.resource_view.subresource_version_info);
		}

		var display_list = [];
		for (var draw_id = 0; draw_id < this.pass_draws_data.length; draw_id++)
		{
			var href = null;
			if (subresource_unique_name && is_output_resource)
			{
				href = `#display_draw_output_resource(${this.pass_id},'${subresource_unique_name}',${draw_id});`;
			}

			display_list.push({
				'name': `${draw_id}: ${this.pass_draws_data[draw_id]['DrawName']}`,
				'href': href
			});
		}

		if (subresource_unique_name)
		{
			var href = null;
			if (is_output_resource)
			{
				href = `#display_output_resource(${this.pass_id},'${subresource_unique_name}');`;
			}

			display_list.push({
				'name': 'Final pass output',
				'href': href
			});
		}

		render_selection_list_html(
			document.getElementById('pass_draw_list'),
			display_list,
			{
				'search': document.getElementById('pass_draw_search').value,
			});
	}

	refresh_resource_modifying_list()
	{
		if (this.resource_view === null || this.resource_view.resource_desc['Desc'] == 'FRDGParameterStruct')
		{
			document.getElementById('resource_pass_modifying_outter').style.display = 'none';
			return;
		}
		document.getElementById('resource_pass_modifying_outter').style.display = 'block';

		var subresource_unique_name = get_subresource_unique_name(this.resource_view.subresource_version_info);
		var resource_desc = get_resource_desc(this.resource_view.subresource_version_info['resource']);

		var display_list = [];
		for (var pass_id = 0; pass_id < g_passes.length; pass_id++)
		{
			var producer = false;
			g_passes[pass_id]['OutputResources'].forEach(function(value) {
				if (value == subresource_unique_name)
				{
					producer = true;
				}
			});

			if (producer)
			{
				display_list.push({
					'name': g_passes[pass_id]['EventName'],
					'href': `#display_output_resource(${pass_id},'${subresource_unique_name}');`,
				});
			}
		}

		document.getElementById('resource_pass_modifying_title').innerHTML = `Passes modifying ${resource_desc['Name']}`;
		render_selection_list_html(
			document.getElementById('resource_pass_modifying_list'),
			display_list,
			{
				'search': document.getElementById('resource_pass_modifying_search').value,
			});
	}

	refresh_resource_reading_list()
	{
		if (this.resource_view === null || this.resource_view.resource_desc['Desc'] == 'FRDGParameterStruct')
		{
			document.getElementById('resource_pass_reading_outter').style.display = 'none';
			return;
		}
		document.getElementById('resource_pass_reading_outter').style.display = 'block';

		var subresource_unique_name = get_subresource_unique_name(this.resource_view.subresource_version_info);
		var resource_desc = get_resource_desc(this.resource_view.subresource_version_info['resource']);

		var display_list = [];
		for (var pass_id = 0; pass_id < g_passes.length; pass_id++)
		{
			var reader = false;
			g_passes[pass_id]['InputResources'].forEach(function(value) {
				if (value == subresource_unique_name)
				{
					reader = true;
				}
			});

			if (reader)
			{
				display_list.push({
					'name': g_passes[pass_id]['EventName'],
					'href': `#display_input_resource(${pass_id},'${subresource_unique_name}');`,
				});
			}
		}

		document.getElementById('resource_pass_reading_title').innerHTML = `Passes reading ${resource_desc['Name']}`;
		render_selection_list_html(
			document.getElementById('resource_pass_reading_list'),
			display_list,
			{
				'search': document.getElementById('resource_pass_reading_search').value,
			});
	}

	resize(ctx)
	{
		if (this.resource_view !== null)
		{
			this.resource_view.resize(ctx);
		}
	}

	set_resource_view(new_resource_view)
	{
		var parent_dom = document.getElementById('display_resource_pannel');
		if (this.resource_view !== null)
		{
			this.resource_view.release();
			delete this.resource_view;
			parent_dom.innerHTML = '';
		}

		this.resource_view = new_resource_view;
		if (this.resource_view !== null)
		{
			this.resource_view.setup_html(parent_dom);
			this.refresh_all_lists();
			onresize_body();
		}
	}

	get navigations()
	{
		var navs = [`display_pass(${this.pass_id});`];

		if (this.resource_view !== null)
		{
			navs.concat(this.resource_view.navigations);
		}

		return navs;
	}

	release()
	{
		this.set_resource_view(null);
	}
}

function display_pass_internal(pass_id)
{
	if (g_view instanceof PassView && pass_id == g_view.pass_id)
	{
		return;
	}

	set_main_view(new PassView(pass_id));
}

function display_resource_internal(subresource_version_info)
{
	var resource_desc = get_resource_desc(subresource_version_info['resource']);
	if (!resource_desc)
	{
		return;
	}

	if (g_view.resource_view !== null && get_subresource_unique_version_name(g_view.resource_view.subresource_version_info) == get_subresource_unique_version_name(subresource_version_info))
	{
		return;
	}

	if (resource_desc['Desc'] == 'FRDGBufferDesc')
	{
		if (resource_desc['Usage'].includes('AccelerationStructure'))
		{
			g_view.set_resource_view(new RaytracingAccelerationStructureView(subresource_version_info, resource_desc));
		}
		else
		{
			g_view.set_resource_view(new BufferView(subresource_version_info, resource_desc));
		}
	}
	else if (resource_desc['Desc'] == 'FRDGTextureDesc' && (resource_desc['Type'] == 'Texture2D' || resource_desc['Type'] == 'Texture2DArray'))
	{
		var new_texture_view = new TextureView(subresource_version_info, resource_desc);

		// Keep the same zoom settings if the subresources are exactly the same extent
		if (g_view && 'resource_view' in g_view && g_view.resource_view instanceof TextureView)
		{
			var zoom_settings = g_view.resource_view.get_zoom_settings();

			if (new_texture_view.is_zoom_settings_compatible(zoom_settings))
			{
				new_texture_view.onload = function() {
					new_texture_view.apply_zoom_settings(zoom_settings);
				};
			}
		}

		g_view.set_resource_view(new_texture_view);
	}
}


// -------------------------------------------------------------------- HREF FUNCTIONS

function display_pass(pass_id)
{
	// Load first resource
	if (g_passes[pass_id]['OutputResources'][0])
	{
		redirect_to_hash(`display_output_resource(${pass_id},'${g_passes[pass_id]['OutputResources'][0]}');`);
	}
	else if (g_passes[pass_id]['InputResources'][0])
	{
		redirect_to_hash(`display_input_resource(${pass_id},'${g_passes[pass_id]['InputResources'][0]}');`);
	}
	else
	{
		display_pass_internal(pass_id);
	}
}

function display_input_resource(pass_id, subresource_unique_name)
{
	var previous_producer_pass = -1;
	for (var i = 0; i < pass_id; i++)
	{
		var cur_outputs = g_passes[i]['OutputResources'];

		cur_outputs.forEach(function(value) {
			if (value == subresource_unique_name)
			{
				previous_producer_pass = i;
			}
		});
	}

	var subresource_version_info = parse_subresource_unique_name(subresource_unique_name);
	if (previous_producer_pass >= 0)
	{
		subresource_version_info['pass'] = g_passes[previous_producer_pass]['Pointer'];
	}
	else
	{
		subresource_version_info['pass'] = k_null_json_ptr;
	}

	display_pass_internal(pass_id);
	display_resource_internal(subresource_version_info);
}

function display_output_resource(pass_id, subresource_unique_name)
{
	var subresource_version_info = parse_subresource_unique_name(subresource_unique_name);
	subresource_version_info['pass'] = g_passes[pass_id]['Pointer'];

	display_pass_internal(pass_id);
	display_resource_internal(subresource_version_info);
}

function display_draw_output_resource(pass_id, subresource_unique_name, draw_id)
{
	var subresource_version_info = parse_subresource_unique_name(subresource_unique_name);
	subresource_version_info['pass'] = g_passes[pass_id]['Pointer'];
	subresource_version_info['draw'] = draw_id;

	display_pass_internal(pass_id);
	display_resource_internal(subresource_version_info);
}


// -------------------------------------------------------------------- DISPLAY VIEWER CONSOLE

var g_console_events = [];
var g_console_error_count = 0;

class ConsoleEvent
{
	constructor(type, message)
	{
		this.type = type;
		this.message = message;
	}
}

class ConsoleView extends IView
{
	constructor()
	{
		super();
	}

	setup_html(parent_dom)
	{
		parent_dom.innerHTML = `
			<div class="main_div">
				<div class="pass_title">Viewer Console</div>
				<div id="console_events_pannel"></div>
			</div>`;

		document.title = 'Viewer Console';
		this.update_console_events();
	}

	update_console_events()
	{
		var html = `
			<table width="100%" class="pretty_table">`;

		for (var console_event of g_console_events)
		{
			html += `
				<tr class="${console_event.type}">
					<td>${console_event.type}: ${console_event.message}</td>
				</tr>`;
		};

		html += `
			</table>`;

		document.getElementById('console_events_pannel').innerHTML = html;
	}

	get navigations()
	{
		return [`display_console();`];
	}
}

function update_console_button()
{
	if (document.getElementById('console_button') && g_console_error_count > 0)
	{
		document.getElementById('console_button').classList.add('error');
		document.getElementById('console_button').innerHTML = `Console (${g_console_error_count} Errors)`;
	}
}

function add_console_event(type, message)
{
	console.assert(['error', 'log'].includes(type));
	g_console_events.push(new ConsoleEvent(type, message));

	if (type == 'error')
	{
		g_console_error_count += 1;
		update_console_button();
	}

	if (g_view instanceof ConsoleView)
	{
		g_view.update_console_events();
	}
}

function display_console(tip_id)
{
	set_main_view(new ConsoleView());
}

function init_console()
{
	window.addEventListener('error', function(event) {
		add_console_event('error', `${event.filename}:${event.fileno}: ${event.message}`);
	});
}


// -------------------------------------------------------------------- DISPLAY INFOS

class InfosView extends IView
{
	constructor()
	{
		super();
	}

	setup_html(parent_dom)
	{
		var info_htmls = '';
		{
			info_htmls += `
				<table width="100%" class="pretty_table">`;
			
			for (var key in g_infos)
			{
			    if (g_infos.hasOwnProperty(key))
			    {
					info_htmls += `
						<tr>
							<td width="100px">${key}</td>
							<td>${g_infos[key]}</td>
						</tr>`;
			    }
			}

			info_htmls += `
				</table>`;
		}

		parent_dom.innerHTML = `
			<div class="main_div">
				<div class="pass_title">Infos</div>
				<div id="infos_pannel">${info_htmls}</div>
			</div>`;

		if (does_file_exists('Base/Screenshot.png'))
		{
			parent_dom.innerHTML += `
				<div class="main_div">
					<div class="pass_title">Screenshot</div>
					<img src="${k_current_dir}/Base/Screenshot.png" style="width: 100%;" />
				</div>`;
		}

		document.title = 'Dump infos';
	}

	get navigations()
	{
		return [`display_infos();`];
	}
}

function display_infos(tip_id)
{
	set_main_view(new InfosView());
}


// -------------------------------------------------------------------- DISPLAY TIP

const k_tips = [
	// Dumping process
	'Can use the CTRL+SHIFT+/ keyboard shortcut to summon the DumpGPU command.',
	'Speed up your frame dump by only selecting the passes you need with r.DumpGPU.Root. For instance r.DumpGPU.Root="*PostProcessing*".',
	'Uses r.DumpGPU.Delay to delay the dump of few seconds to have time to repro the issue with gameplay logic (for instance moving arround in the map).',
	'Uses r.DumpGPU.FrameCount to dump more than one frame. This is useful when artifact may be produced only some frames but end up stucked in temporal histories.',
	'Uses r.DumpGPU.FixedTickRate to automatically override the engine\'s tick rate to fixed value when dumping multiple frames.',
	'Uses r.DumpGPU.DumpOnScreenshotTest=1 to automatically produce a GPU dump of the frame producing screenshot in the automated AScreenshotFunctionalTest',
	'GPU dumps can be large and accumulate on your hard drive in your various projects\' Saved/ directories. Set r.DumpGPU.Directory="D:/tmp/DumpGPU/" in your console variables or UE-DumpGPUPath environment variable to dump them all at the same location on your machine.',
	'Uses -cvarsini to override console variables with your own ConsoleVariables.ini file on a cooked build.',

	// General navigations
	'All navigation links can be open in a new tab.',
	'Uses the browser\'s back button to navigate to previously inspected resource.',
	'Make sure to browse the dump informations.',
	'Make sure to browse the console variables.',
	'Make sure to browse the log file.',
	'Share the part of the URL after the # (for instance #display_input_resource(96,\'TSR.AntiAliasing.Noise.000000006b9b2c00.mip0\');) to anyone else whom have this GPU dump to so they too can navigate to the exact same resource view.',
	'Set r.DumpGPU.Viewer.Visualize in your ConsoleVariables.ini so the dump viewer automatically open this RDG output resource at startup.',

	// Buffer visualization
	'Uses the templated FRDGBufferDesc::Create*<FMyStructure>(NumElements) to display your buffer more conveniently with FMyStructure layout in the buffer visualization.',
	'Buffer visualization supports float, half, int, uint, short, ushort, char, uchar, as well as hexadecimal with hex(uint) and binary with bin(uint).',
	'Uses the "Address..." field at the very left of the buffer view\'s header to navigate to a specific address. Supports decimal and 0x prefixed hexadecimal notations.',

	// Texture visualization
	'Click the texture viewer to then zoom in and out with your mouse wheel.',
	'Right click to drag texture viewport arround when zoomed in.',
	'Uses the texture viewer\'s "copy to clipboard" to share your texture visualization to anyone.',
];

class TipView extends IView
{
	constructor(tip_id)
	{
		super();
		this.tip_id = tip_id;
	}

	setup_html(parent_dom)
	{
		parent_dom.innerHTML = `
			<div style="padding-top: 20%; width: 50%; margin: 0 auto; font-size: 14;">
				<div>User tip:</div>
				<div style="padding: 20px;">${k_tips[this.tip_id]}</div>
				<div class="button_list" style="margin-left: auto; display: block; width: 100px;">
						<a href="#display_tip(${(this.tip_id + 1) % k_tips.length});">Next</a>
				</div>
			</div>`;
	}

	get navigations()
	{
		return [`display_tip(${this.tip_id});`];
	}
}

function display_tip(tip_id)
{
	if (tip_id === undefined)
	{
		tip_id = Math.floor(Math.random() * k_tips.length);
		document.title = 'GPU Dump Viewer';
	}
	else
	{
		document.title = `User tip #${tip_id + 1}`;
	}

	set_main_view(new TipView(tip_id));
}


// -------------------------------------------------------------------- DISPLAY CONSOLE VARIABLES

class CVarsView extends IView
{
	constructor()
	{
		super();
		this.load_cvars();
	}

	setup_html(parent_dom)
	{
		parent_dom.innerHTML = `
			<div class="main_div">
				<div class="pass_title">Console variables</div>
				<div class="selection_list_search">
					<input type="search" id="cvars_search_input" oninput="g_view.refresh_cvars();" onchange="g_view.refresh_cvars();" style="width: 100%;" placeholder="Search console variables..." />
				</div>
				<div id="cvars_pannel"></div>
			</div>`;

		this.refresh_cvars();
		document.title = 'Console variables';
	}

	load_cvars()
	{
		var cvars_csv = load_text_file('Base/ConsoleVariables.csv');

		this.cvars = [];
		var cvars_set = new Set();
		var csv_lines = cvars_csv.split('\n');
		for (var i = 1; i < csv_lines.length - 1; i++)
		{
			var csv_line = csv_lines[i].split(',');
			var entry = {};
			entry['name'] = csv_line[0];
			entry['type'] = csv_line[1];
			entry['set_by'] = csv_line[2];
			entry['value'] = csv_line[3];

			if (!cvars_set.has(entry['name']))
			{
				cvars_set.add(entry['name']);
				this.cvars.push(entry);
			}
		}

		this.cvars.sort(function(a, b)
		{
			if (a['name'] < b['name'])
			{
				return -1;
			}
			else if (a['name'] > b['name'])
			{
				return 1;
			}
			return 0;
		});
	}

	refresh_cvars()
	{
		var html = `
			<table width="100%" class="pretty_table">
				<tr class="header">
					<td>Name</td>
					<td width="15%">Value</td>
					<td width="30px">Type</td>
					<td width="100px">Set by</td>
				</tr>`;

		var cvar_search = document.getElementById('cvars_search_input').value.toLowerCase();

		this.cvars.forEach(function(cvar)
		{
			var display = true;
			if (cvar_search)
			{
				display = cvar['name'].toLowerCase().includes(cvar_search);
			}

			if (display)
			{
				html += `
					<tr>
						<td>${cvar['name']}</td>
						<td>${cvar['value']}</td>
						<td>${cvar['type']}</td>
						<td>${cvar['set_by']}</td>
					</tr>`;
			}
		});

		html += `
			</table>`;

		document.getElementById('cvars_pannel').innerHTML = html;
	}

	get navigations()
	{
		return [`display_cvars();`];
	}

	release()
	{
		this.cvars = null;
	}
}

function display_cvars()
{
	set_main_view(new CVarsView());
}


// -------------------------------------------------------------------- DISPLAY LOG

function get_log_path()
{
	return `Base/${g_infos['LogFilename']}`;
}

class LogView extends IView
{
	constructor()
	{
		super();

		this.log = [];

		var log = load_text_file(get_log_path());
		if (log)
		{
			this.log = log.split('\n');
		}
	}

	setup_html(parent_dom)
	{
		parent_dom.innerHTML = `
			<div class="main_div">
				<div class="pass_title">${g_infos['LogFilename']}</div>
				<div class="selection_list_search">
					<input type="search" id="log_search_input" oninput="g_view.refresh_log();" onchange="g_view.refresh_log();" style="width: 100%;" placeholder="Search log..." />
				</div>
				<div id="log_pannel"></div>
			</div>`;

		this.refresh_log();
		document.title = 'Log';
	}

	refresh_log()
	{
		var html = `
			<table width="100%" class="pretty_table">`;

		var log_search = document.getElementById('log_search_input').value.toLowerCase();

		this.log.forEach(function(log_line)
		{
			var display = true;
			if (log_search)
			{
				display = log_line.toLowerCase().includes(log_search);
			}

			if (display)
			{
				html += `
					<tr>
						<td>${log_line}</td>
					</tr>`;
			}
		});

		html += `
			</table>`;

		document.getElementById('log_pannel').innerHTML = html;
	}

	get navigations()
	{
		return [`display_log();`];
	}

	resize(ctx)
	{
		document.getElementById('log_pannel').style.width = `${ctx.width - 2 * k_style_padding_size}px`;
		document.getElementById('log_pannel').style.height = `${ctx.height - 2 * k_style_padding_size - 75}px`;
		document.getElementById('log_pannel').style.overflow = `scroll`;
	}

	release()
	{
		this.log = null;
	}
}

function display_log()
{
	set_main_view(new LogView());
}



// -------------------------------------------------------------------- WEBGL UTILS

function copy_canvas_to_clipboard(canvas, text)
{
	canvas.toBlob(function(image_blob) {
		var text_blob = new Blob([text], { type: 'text/plain' });
		var clipboard_data = {
			[text_blob.type]: text_blob,
			[image_blob.type]: image_blob,
		};

		navigator.clipboard.write([new ClipboardItem(clipboard_data)]);
	}, 'image/png');
}

function gl_create_vertex_buffer(gl, vertices)
{
	var webgl_vertex_buffer = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, webgl_vertex_buffer);
	gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
	gl.bindBuffer(gl.ARRAY_BUFFER, null);
	return webgl_vertex_buffer;
}

function gl_create_index_buffer(gl, indices)
{
	var webgl_index_buffer = gl.createBuffer();
	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, webgl_index_buffer);
	gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
	return webgl_index_buffer;
}

function gl_create_shader(gl, shader_type, shader_code)
{
	const shader_std = `#version 300 es

precision highp float;
precision highp int;
precision highp sampler2D;
precision highp usampler2D;

struct FloatInfos
{
	bool is_denormal;
	bool is_infinity;
	bool is_nan;
};

FloatInfos decodeFloat(float x)
{
	const uint mantissa_bit_count = 23u;
	const uint exp_bit_count = 8u;

	uint raw = floatBitsToUint(x);

	uint sign_bit      = (raw >> (mantissa_bit_count + exp_bit_count)) & 0x1u;
	uint mantissa_bits = (raw >> 0u)                     & ((0x1u << mantissa_bit_count) - 1u);
	uint exp_bits      = (raw >> mantissa_bit_count)     & ((0x1u << exp_bit_count) - 1u);

	bool is_max_exp = exp_bits == ((0x1u << exp_bit_count) - 1u);

	FloatInfos infos;
	infos.is_denormal = exp_bits == 0u;
	infos.is_infinity = is_max_exp && mantissa_bits == 0u;
	infos.is_nan = is_max_exp && mantissa_bits != 0u;

	return infos;
}

#define isnan(x) decodeFloat(x).is_nan
#define isinf(x) decodeFloat(x).is_infinity
#define isdenorm(x) decodeFloat(x).is_denormal

`;
	
	var gl_shader = gl.createShader(shader_type);
	gl.shaderSource(gl_shader, shader_std + shader_code);
	gl.compileShader(gl_shader);

	var compilation_status = gl.getShaderParameter(gl_shader, gl.COMPILE_STATUS);
	if (!compilation_status)
	{
		var compilation_log = gl.getShaderInfoLog(gl_shader);
		console.error('Shader compiler log: ' + compilation_log);
		return compilation_log;
	}
	return gl_shader;
}

function gl_create_shader_program(gl, vert_shader, frag_shader)
{
	var gl_vert_shader = null;
	if (typeof vert_shader === 'string')
	{
		gl_vert_shader = gl_create_shader(gl, gl.VERTEX_SHADER, vert_shader);
		if (typeof gl_vert_shader === 'string')
		{
			return gl_vert_shader;
		}
	}
	else if (typeof vert_shader === 'WebGLShader')
	{
		gl_vert_shader = vert_shader;
	}
	else
	{
		console.error('wrong vert_shader');
		return null;
	}

	var gl_frag_shader = null;
	if (typeof frag_shader === 'string')
	{
		gl_frag_shader = gl_create_shader(gl, gl.FRAGMENT_SHADER, frag_shader);
		if (typeof gl_frag_shader === 'string')
		{
			return gl_frag_shader;
		}
	}
	else if (typeof frag_shader === 'WebGLShader')
	{
		gl_frag_shader = frag_shader;
	}
	else
	{
		console.error('wrong vert_shader');
		return null;
	}

	var shader_program = gl.createProgram();
	gl.attachShader(shader_program, gl_vert_shader);
	gl.attachShader(shader_program, gl_frag_shader);
	gl.linkProgram(shader_program);

	var link_status = gl.getProgramParameter(shader_program, gl.LINK_STATUS);
	if (!link_status)
	{
		var link_log = gl.getProgramInfoLog(shader_program);
		console.error('Program compiler log: ' + link_log);
		return null;
	}
	return shader_program;
}

function gl_set_uniform_uint(gl, program, name, value)
{
	var uniform_loc = gl.getUniformLocation(program, name);
	if (uniform_loc)
	{
		gl.uniform1ui(uniform_loc, value);
	}
}

function gl_set_uniform_mat4(gl, program, name, matrix)
{
	var uniform_loc = gl.getUniformLocation(program, name);
	if (uniform_loc)
	{
		gl.uniformMatrix4fv(uniform_loc, false, new Float32Array(matrix));
	}
}


// -------------------------------------------------------------------- MATH UTILS

function create_null_matrix()
{
	return new Array(4 * 4).fill(0.0);
}

function create_identity_matrix()
{
	var matrix = create_null_matrix();
	matrix[0 + 0 * 4] = 1.0;
	matrix[1 + 1 * 4] = 1.0;
	matrix[2 + 2 * 4] = 1.0;
	matrix[3 + 3 * 4] = 1.0;
	return matrix;
}

function create_view_matrix(camera_pos, camera_dir)
{
	var camera_pos_x = camera_pos[0];
	var camera_pos_y = camera_pos[1];
	var camera_pos_z = camera_pos[2];

	var camera_dir_len = Math.sqrt(
		camera_dir[0] * camera_dir[0] +
		camera_dir[1] * camera_dir[1] +
		camera_dir[2] * camera_dir[2]);
	var camera_dir_x = camera_dir[0] / camera_dir_len;
	var camera_dir_y = camera_dir[1] / camera_dir_len;
	var camera_dir_z = camera_dir[2] / camera_dir_len;

	var horizon_dir_len = Math.sqrt(camera_dir_x * camera_dir_x + camera_dir_y * camera_dir_y);
	var horizon_dir_x = camera_dir_y / horizon_dir_len;
	var horizon_dir_y = -camera_dir_x / horizon_dir_len;
	var horizon_dir_z = 0.0;

	var vertical_dir_x = - camera_dir_y * horizon_dir_z + camera_dir_z * horizon_dir_y;
	var vertical_dir_y = - camera_dir_z * horizon_dir_x + camera_dir_x * horizon_dir_z;
	var vertical_dir_z = - camera_dir_x * horizon_dir_y + camera_dir_y * horizon_dir_x;

	// column major
	var matrix = create_identity_matrix();
	matrix[0 + 0 * 4] = horizon_dir_x;
	matrix[0 + 1 * 4] = horizon_dir_y;
	matrix[0 + 2 * 4] = horizon_dir_z;
	matrix[0 + 3 * 4] = -(horizon_dir_x * camera_pos_x + horizon_dir_y * camera_pos_y + horizon_dir_z * camera_pos_z);

	matrix[1 + 0 * 4] = vertical_dir_x;
	matrix[1 + 1 * 4] = vertical_dir_y;
	matrix[1 + 2 * 4] = vertical_dir_z;
	matrix[1 + 3 * 4] = -(vertical_dir_x * camera_pos_x + vertical_dir_y * camera_pos_y + vertical_dir_z * camera_pos_z);

	matrix[2 + 0 * 4] = camera_dir_x;
	matrix[2 + 1 * 4] = camera_dir_y;
	matrix[2 + 2 * 4] = camera_dir_z;
	matrix[2 + 3 * 4] = -(camera_dir_x * camera_pos_x + camera_dir_y * camera_pos_y + camera_dir_z * camera_pos_z);

	return matrix;
}

// Matches TPerspectiveMatrix
function create_projection_matrix_reverse_z_persepective(half_fov, aspect_ratio, clipping_plane)
{
	const z_precision = 0.0;

	var half_fov_x = half_fov * Math.PI / 180.0;
	var half_fov_y = half_fov_x;
	var mult_fov_x = 1.0;
	var mult_fov_y = aspect_ratio;
	var min_z = clipping_plane;
	var max_z = clipping_plane;

	// column major
	var matrix = create_null_matrix();
	matrix[0 + 0 * 4] = mult_fov_x / Math.tan(half_fov_x);
	matrix[1 + 1 * 4] = mult_fov_y / Math.tan(half_fov_y);
	matrix[2 + 3 * 4] = min_z;
	matrix[3 + 2 * 4] = 1.0;

	return matrix;
}


// -------------------------------------------------------------------- DISPLAY TEXTURE

var k_exotic_raw_channel_bit_depth = 0;
var g_shader_code_dict = {};

class TextureView extends ResourceView
{
	constructor(subresource_version_info, resource_desc)
	{
		super(subresource_version_info, resource_desc);
		this.is_ready = false;
		this.raw_texture_data = null;
		this.subresource_desc = null;
		this.release_gl();
		this.viewport_width = 1;
		this.pixel_scaling = 0;
		this.viewport_selected = false;
		this.is_draging_canvas = false;
		this.src_channels = 'RGB';
		this.src_color_space = 'sRGB/Rec709';
		this.display_mode = 'Visualization';
	}

	setup_html(parent_dom)
	{
		var subresource_extent = this.get_subresource_extent();

		var dpi = window.devicePixelRatio || 1;

		var canvas_rendering_res_x = subresource_extent['x'];
		var canvas_rendering_res_y = subresource_extent['y'];

		var canvas_display_w = canvas_rendering_res_x / dpi;
		var canvas_display_h = canvas_rendering_res_y / dpi;

		var resource_info_htmls = '';
		{
			resource_info_htmls += `
				<table width="100%" class="pretty_table resource_desc">`;
			
			for (var key in this.resource_desc)
			{
			    if (this.resource_desc.hasOwnProperty(key)){
					resource_info_htmls += `
						<tr>
							<td>${key}</td>
							<td>${this.resource_desc[key]}</td>
						</tr>`;
			    }
			}

			resource_info_htmls += `
				</table>`;
		}

		var title = prettify_subresource_unique_name(this.subresource_version_info, this.resource_desc);
		if (this.subresource_version_info['draw'] >= 0)
		{
			title += ` draw:${this.subresource_version_info['draw']}`;
		}

		var html = `
			<div class="main_div">
				<div class="selection_list_title">Texture visualization: ${title}</div>
				<div id="canvas_outter" style="margin-bottom: 10px;">
					<div id="canvas_header">
						<div class="button_list" id="texture_visualization_change_pixel_scaling"><!---
							---><input type="button" onclick="g_view.resource_view.change_pixel_scaling(this);" value="Fit"/><!--- 
							---><input type="button" onclick="g_view.resource_view.change_pixel_scaling(this);" value="1:1"/><!--- 
							---><input type="button" onclick="g_view.resource_view.change_pixel_scaling(this);" value="2:1"/><!--- 
							---><input type="button" onclick="g_view.resource_view.change_pixel_scaling(this);" value="4:1"/><!--- 
							---><input type="button" onclick="g_view.resource_view.change_pixel_scaling(this);" value="8:1"/><!--- 
							---><input type="button" onclick="g_view.resource_view.change_pixel_scaling(this);" value="16:1"/>
						</div>
						<div class="button_list" id="texture_visualization_change_src_channels"><!---
							---><input type="button" onclick="g_view.resource_view.change_src_channels(this);" value="RGB" title="Display the WebGL's visualization shader's display.rgb channels (default)."/><!---
							---><input type="button" onclick="g_view.resource_view.change_src_channels(this);" value="R" title="Display the WebGL's visualization shader's display.r channel."/><!---
							---><input type="button" onclick="g_view.resource_view.change_src_channels(this);" value="G" title="Display the WebGL's visualization shader's display.g channel."/><!---
							---><input type="button" onclick="g_view.resource_view.change_src_channels(this);" value="B" title="Display the WebGL's visualization shader's display.b channel."/><!---
							---><input type="button" onclick="g_view.resource_view.change_src_channels(this);" value="A" title="Display the WebGL's visualization shader's display.a channel."/>
						</div>
						<div class="button_list" id="texture_visualization_change_src_color"><!---
							---><span>Src color:</span><!---
							---><input type="button" onclick="g_view.resource_view.change_src_color(this);" value="sRGB/Rec709" title="Performs no modifications to display to WebGL's sRGB backbuffer (default)."/><!---
							---><input type="button" onclick="g_view.resource_view.change_src_color(this);" value="RGB Linear[0;1]" title="Applies a pow(display.rgb, 2.2); to output WebGL's sRGB backbuffer."/>
						</div>
						<div class="button_list" id="texture_visualization_change_display_modes"><!---
							---><input type="button" onclick="g_view.resource_view.change_display_mode(this);" value="Visualization" title="Display the WebGL's visualization shader's display.rgb."/><!---
							---><input type="button" onclick="g_view.resource_view.change_display_mode(this);" value="NaN" title="Display any NaN in the raw texture."/><!---
							---><input type="button" onclick="g_view.resource_view.change_display_mode(this);" value="Inf" title="Display any Inf in the raw texture."/>
						</div>
						<div class="button_list">
							<input type="button" onclick="g_view.resource_view.copy_canvas_to_clipboard();" value="Copy to clipboard"/>
						</div>
					</div>
					<div id="canvas_viewport">
						<canvas
							id="texture_visualization_canvas"
							width="${canvas_rendering_res_x}"
							height="${canvas_rendering_res_y}"
							style="width: ${canvas_display_w}px; height: ${canvas_display_h}px;"></canvas>
					</div>
					<div id="canvas_footer">
						<table>
							<tr>
								<td>Cursor texel pos:</td>
								<td id="canvas_hover_texel_info"></td>
							</tr>
							<tr>
								<td>Selected texel pos:</td>
								<td id="canvas_selected_texel_info"></td>
							</tr>
						</table>
					</div>
				</div>
				<table width="100%">
					<tr>
						<td width="50%">
							<div class="selection_list_title">Texture descriptor</div>
							${resource_info_htmls}
						</td>
						<td style="width: 50%;">
							<textarea id="texture_visualization_code_input" oninput="g_view.resource_view.shader_user_code_change();" onchange="g_view.resource_view.shader_user_code_change();"></textarea>
							<textarea id="texture_visualization_code_log" readonly></textarea>
							<div style="padding: 3px 10px;"><a class="external_link" href="https://www.khronos.org/files/webgl20-reference-guide.pdf">WebGL 2.0's quick reference card</a></div>
						</td>
					</tr>
				</table>
			</div>`;

		parent_dom.innerHTML = html;

		update_value_selection(document.getElementById('texture_visualization_change_pixel_scaling'), `Fit`);
		update_value_selection(document.getElementById('texture_visualization_change_src_channels'), this.src_channels);
		update_value_selection(document.getElementById('texture_visualization_change_src_color'), this.src_color_space);
		update_value_selection(document.getElementById('texture_visualization_change_display_modes'), this.display_mode);

		// Init WebGL 2.0
		this.init_gl();

		// Translate the descriptor of the subresource.
		this.subresource_desc = this.translate_subresource_desc();
		if (this.subresource_desc === null)
		{
			return;
		}

		// Setup mouse motion callback
		{
			var texture_view = this;

			this.canvas.onclick = function(event) { texture_view.canvas_click(event); }
			this.canvas.onmouseenter = function(event) { texture_view.canvas_onmousemove(event); };
			this.canvas.onmousemove = function(event) { texture_view.canvas_onmousemove(event); };
			this.canvas.onmousedown = function(event) { texture_view.canvas_onmousedown(event); };
			this.canvas.onmouseup = function(event) { texture_view.canvas_onmouseup(event); };
			this.canvas.oncontextmenu = function(event) { return false; };

			document.getElementById('canvas_viewport').oncontextmenu = function(event) { return false; };

			document.getElementById('canvas_outter').onclick = function(event) { texture_view.onclick_canvas_outter(); };
			document.getElementById('canvas_outter').onmouseleave = function(event) { texture_view.set_select_viewport(false); };
		}

		if (this.subresource_desc['raw_channel_format'] == 'float')
		{
			update_value_selection(document.getElementById('texture_visualization_change_display_modes'), this.display_mode);
		}
		else
		{
			document.getElementById('texture_visualization_change_display_modes').style.display = 'none';
		}

		// Fill out the default visualization shader code in the textarea for the user to customise.
		{
			var user_shader_code_dom = document.getElementById('texture_visualization_code_input');

			if (this.shader_code_saving_key in g_shader_code_dict)
			{
				user_shader_code_dom.value = g_shader_code_dict[this.shader_code_saving_key];
			}
			else
			{
				if (!this.subresource_desc)
				{
					user_shader_code_dom.value = `// Sorry but ${this.resource_desc['Format']} is not supported for display :(`;
				}
				else
				{
					user_shader_code_dom.value = this.subresource_desc['webgl_pixel_shader_public'];
				}
			}
		}

		var texture_view = this;
		var subresource_version_name = get_subresource_unique_version_name(this.subresource_version_info);
		load_resource_binary_file(`Resources/${subresource_version_name}.bin`, function(raw_texture_data)
		{
			if (raw_texture_data)
			{
				texture_view.is_ready = true;
				texture_view.raw_texture_data = raw_texture_data;
				texture_view.resize_canvas(texture_view.pixel_scaling);
				texture_view.process_texture_data_for_visualization();
				texture_view.refresh_texture_visualization();
				texture_view.onload();
			}
			else
			{
				texture_view.error(`Error: Resources/${subresource_version_name}.bin couldn't be loaded.`);
			}
		});

		document.title = `${g_view.pass_data['EventName']} - ${this.resource_desc['Name']} ${this.subresource_version_info['subresource']}`;
	}

	error(error_msg)
	{
		console.error(error_msg);
		document.getElementById('canvas_viewport').innerHTML = `
			<div class="error_msg">
				${error_msg}
			</div>`;
		document.getElementById('texture_visualization_code_input').readOnly = true;
	}

	image_load_uint(texel_x, texel_y)
	{
		console.assert(this.is_ready);

		var extent = this.get_subresource_extent();
		var pixel_data_offset = texel_x + (extent['y'] - 1 - texel_y) * extent['x'];

		var texel_value = [0];
		for (var c = 1; c < this.subresource_desc['raw_channel_count']; c++)
		{
			texel_value.push(0);
		}

		if (this.subresource_desc['webgl_src_type'] === this.gl.UNSIGNED_SHORT_5_6_5)
		{
			var data = new Uint16Array(this.raw_texture_data, pixel_data_offset * 2, 1);

			texel_value[0] = (data[0] >>  0) & 0x1F;
			texel_value[1] = (data[0] >>  5) & 0x3F;
			texel_value[2] = (data[0] >> 11) & 0x1F;
		}
		else if (this.subresource_desc['webgl_src_type'] === this.gl.UNSIGNED_INT_10F_11F_11F_REV)
		{
			var data = new Uint32Array(this.raw_texture_data, pixel_data_offset * 4, 1);

			texel_value[0] = (data[0] >>  0) & 0x7FF;
			texel_value[1] = (data[0] >> 11) & 0x7FF;
			texel_value[2] = (data[0] >> 22) & 0x3FF;
		}
		else if (this.subresource_desc['webgl_src_type'] === this.gl.UNSIGNED_INT_2_10_10_10_REV)
		{
			var data = new Uint32Array(this.raw_texture_data, pixel_data_offset * 4, 1);

			texel_value[0] = (data[0] >>  0) & 0x3FF;
			texel_value[1] = (data[0] >> 10) & 0x3FF;
			texel_value[2] = (data[0] >> 20) & 0x3FF;
			texel_value[3] = (data[0] >> 30) &   0x3;
		}
		else
		{
			var data = null;
			if (this.subresource_desc['raw_channel_bit_depth'] === 8)
			{
				data = new Uint8Array(this.raw_texture_data, this.subresource_desc['raw_channel_count'] * pixel_data_offset * 1, this.subresource_desc['raw_channel_count']);
			}
			else if (this.subresource_desc['raw_channel_bit_depth'] === 16)
			{
				data = new Uint16Array(this.raw_texture_data, this.subresource_desc['raw_channel_count'] * pixel_data_offset * 2, this.subresource_desc['raw_channel_count']);
			}
			else if (this.subresource_desc['raw_channel_bit_depth'] === 32)
			{
				data = new Uint32Array(this.raw_texture_data, this.subresource_desc['raw_channel_count'] * pixel_data_offset * 4, this.subresource_desc['raw_channel_count']);
			}
			else
			{
				return;
			}

			for (var c = 0; c < this.subresource_desc['raw_channel_count']; c++)
			{
				texel_value[c] = data[this.subresource_desc['raw_channel_swizzling'][c]];
			}
		}

		return texel_value;
	}

	decode_texel_uint(value)
	{
		console.assert(this.is_ready);

		value = [...value];

		if (this.subresource_desc['ue_pixel_format'] === 'PF_FloatRGB' || this.subresource_desc['ue_pixel_format'] === 'PF_FloatR11G11B10')
		{
			value[0] = decode_float11(value[0]);
			value[1] = decode_float11(value[1]);
			value[2] = decode_float10(value[2]);
		}
		else if (this.subresource_desc['ue_pixel_format'] === 'PF_A2B10G10R10')
		{
			value[0] = value[0] / 1023.0;
			value[1] = value[1] / 1023.0;
			value[2] = value[2] / 1023.0;
			value[3] = value[3] / 3.0;
		}
		else if (this.subresource_desc['ue_pixel_format'] === 'PF_R5G6B5_UNORM')
		{
			value[0] = value[0] / 31.0;
			value[1] = value[1] / 63.0;
			value[2] = value[2] / 31.0;
		}
		else if (this.subresource_desc['raw_channel_format'] == 'uint')
		{
			// NOP
		}
		else if (this.subresource_desc['raw_channel_format'] == 'float')
		{
			for (var c = 0; c < value.length; c++)
			{
				if (this.subresource_desc['raw_channel_bit_depth'] == 16)
				{
					value[c] = decode_float16(value[c]);
				}
				else if (this.subresource_desc['raw_channel_bit_depth'] == 32)
				{
					value[c] = decode_float32(value[c]);
				}
				else
				{
					console.error('Unknown float bit depth');
				}
			}
		}
		else if (this.subresource_desc['raw_channel_format'] == 'unorm')
		{
			var divide = Number((BigInt(1) << BigInt(this.subresource_desc['raw_channel_bit_depth'])) - 1n);

			for (var c = 0; c < value.length; c++)
			{
				value[c] = Number(value[c]) / divide;
			}
		}
		else
		{
			console.error('Unknown pixel format to convert');
		}

		return value;
	}

	update_texel_cursor_pos(texel_x, texel_y, parent_dom_name)
	{
		console.assert(this.is_ready);

		var parent_dom = document.getElementById(parent_dom_name);
		parent_dom.innerHTML = `<span style="width: 40px; display: inline-block; text-align: right;">${texel_x}</span> <span style="width: 40px; display: inline-block; text-align: right; margin-right: 60px;">${texel_y}</span> `;

		// Find out pixel value.
		{
			var texel_raw = this.image_load_uint(texel_x, texel_y);
			var texel_value = this.decode_texel_uint(texel_raw);

			const channel_name = ['R', 'G', 'B', 'A'];
			const channel_color = ['rgb(255, 38, 38)', 'rgb(38, 255, 38)', 'rgb(38, 187, 255)', 'white'];

			var texel_string = [];
			for (var c = 0; c < this.subresource_desc['raw_channel_count']; c++)
			{
				texel_string.push(`${texel_value[c].toString()} (0x${texel_raw[c].toString(16)})`);
			}

			if (this.subresource_version_info['subresource'] == 'stencil')
			{
				texel_string[0] = '0b' + texel_value[0].toString(2).padStart(8, 0);
			}

			var html = ``;
			for (var c = 0; c < this.subresource_desc['raw_channel_count']; c++)
			{
				html += `<span style="margin-left: 20px; color: ${channel_color[c]};">${channel_name[c]}: <span style="min-width: 170px; display: inline-block; text-align: right;">${texel_string[c]}</span></span>`;
			}

			parent_dom.innerHTML += html;
		}
	}

	get_texel_coordinate_of_mouse(event)
	{
		console.assert(this.is_ready);
		
		var rect = event.target.getBoundingClientRect();
		var html_x = event.clientX - rect.left;
		var html_y = event.clientY - rect.top;

		var texel_x = Math.floor(html_x * px_string_to_int(this.canvas.width) / px_string_to_int(this.canvas.style.width));
		var texel_y = Math.floor(html_y * px_string_to_int(this.canvas.height) / px_string_to_int(this.canvas.style.height));

		return [texel_x, texel_y];
	}

	onclick_canvas_outter()
	{
		if (!this.is_ready)
		{
			return;
		}

		this.set_select_viewport(true);
	}

	onmousewheel(event)
	{
		if (!this.is_ready)
		{
			return;
		}

		if (this.viewport_selected == false)
		{
			// forward scroll event to parent
			document.getElementById('main_right_pannel').scrollTop += event.deltaY;
			return false;
		}

		var dpi = window.devicePixelRatio || 1;
		var zooming_in = event.deltaY < 0.0;

		var subresource_extent = this.get_subresource_extent();

		// Find new pixel scaling to scale the viewport to.
		var new_pixel_scaling = 0;
		{
			var viewport_width = (px_string_to_int(document.getElementById('canvas_viewport').style.width) - k_style_scroll_width) * dpi;

			var min_pixel_scaling = Math.ceil(viewport_width / subresource_extent['x']);
			if (min_pixel_scaling <= 1)
			{
				min_pixel_scaling = 1;
			}
			else if (min_pixel_scaling <= 2)
			{
				min_pixel_scaling = 2;
			}
			else if (min_pixel_scaling <= 4)
			{
				min_pixel_scaling = 4;
			}
			else if (min_pixel_scaling <= 8)
			{
				min_pixel_scaling = 8;
			}
			else if (min_pixel_scaling <= 16)
			{
				min_pixel_scaling = 16;
			}

			if (zooming_in)
			{
				if (this.pixel_scaling == 0)
				{
					if (min_pixel_scaling <= 16)
					{
						new_pixel_scaling = min_pixel_scaling;
					}
				}
				else
				{
					new_pixel_scaling = this.pixel_scaling * 2;
				}
			}
			else
			{
				if (this.pixel_scaling <= min_pixel_scaling)
				{
					new_pixel_scaling = 0;
				}
				else
				{
					new_pixel_scaling = this.pixel_scaling / 2;
				}
			}

			new_pixel_scaling = Math.min(Math.max(new_pixel_scaling, 0), 16);
		}

		var [texel_x, texel_y] = this.get_texel_coordinate_of_mouse(event);

		this.resize_canvas(new_pixel_scaling, texel_x, texel_y);
		this.update_change_pixel_scaling_button();

		return false;
	}

	update_change_pixel_scaling_button()
	{
		const pixel_scaling_names = {
			0: 'Fit',
			1: '1:1',
			2: '2:1',
			4: '4:1',
			8: '8:1',
			16: '16:1',
		};
		update_value_selection(document.getElementById('texture_visualization_change_pixel_scaling'), pixel_scaling_names[this.pixel_scaling]);
	}

	canvas_click(event)
	{
		if (!this.is_ready)
		{
			return;
		}
		
		var subresource_extent = this.get_subresource_extent();

		var [texel_x, texel_y] = this.get_texel_coordinate_of_mouse(event);

		texel_x = Math.min(Math.max(texel_x, 0), subresource_extent['x'] - 1);
		texel_y = Math.min(Math.max(texel_y, 0), subresource_extent['y'] - 1);

		this.update_texel_cursor_pos(texel_x, texel_y, 'canvas_selected_texel_info');
	}

	canvas_onmousemove(event)
	{
		if (!this.is_ready)
		{
			return;
		}
		
		if (this.is_draging_canvas)
		{
			this.drag_canvas(event);
		}

		var subresource_extent = this.get_subresource_extent();

		var [texel_x, texel_y] = this.get_texel_coordinate_of_mouse(event);

		texel_x = Math.min(Math.max(texel_x, 0), subresource_extent['x'] - 1);
		texel_y = Math.min(Math.max(texel_y, 0), subresource_extent['y'] - 1);

		this.update_texel_cursor_pos(texel_x, texel_y, 'canvas_hover_texel_info');
	}

	canvas_onmousedown(event)
	{
		if (!this.is_ready)
		{
			return;
		}
		
		if (event.button == 2)
		{
			this.set_select_viewport(true);
			this.is_draging_canvas = true;
		}
	}

	canvas_onmouseup(event)
	{
		if (!this.is_ready)
		{
			return;
		}
		
		if (event.button == 2)
		{
			this.is_draging_canvas = false;
		}
	}

	drag_canvas(event)
	{
		if (!this.is_ready)
		{
			return;
		}
		
		var canvas_viewport = document.getElementById('canvas_viewport');
		canvas_viewport.scrollLeft -= event.movementX;
		canvas_viewport.scrollTop -= event.movementY;
	}

	set_select_viewport(selected)
	{
		if (this.viewport_selected === selected)
		{
			return;
		}

		this.viewport_selected = selected;
		if (this.viewport_selected)
		{
			var texture_view = this;
			document.getElementById('main_right_pannel').onwheel = function(event) { return false; };
			document.getElementById('canvas_outter').classList.add('selected');
			document.getElementById('canvas_viewport').style.overflow = 'scroll';
			this.canvas.onwheel = function(event) { return texture_view.onmousewheel(event); };
		}
		else
		{
			document.getElementById('main_right_pannel').onwheel = null;
			document.getElementById('canvas_outter').classList.remove('selected');
			document.getElementById('canvas_viewport').style.overflow = 'hidden';
			this.canvas.onwheel = null;
			this.is_draging_canvas = false;
		}
	}

	resize(ctx)
	{
		var dpi = window.devicePixelRatio || 1;
		var viewport_width = ctx.width - 2 * k_style_padding_size - 2;
		var viewport_width_no_scroll = viewport_width - k_style_scroll_width;

		var subresource_extent = this.get_subresource_extent();
		var canvas_rendering_res_x = subresource_extent['x'];
		var canvas_rendering_res_y = subresource_extent['y'];

		var canvas_outter = document.getElementById('canvas_outter');
		var canvas_viewport = document.getElementById('canvas_viewport');
		canvas_outter.style.width = `${viewport_width}px`;
		canvas_viewport.style.width = `${viewport_width_no_scroll + k_style_scroll_width}px`;
		canvas_viewport.style.height = `${viewport_width_no_scroll * canvas_rendering_res_y / canvas_rendering_res_x + k_style_scroll_width}px`;

		this.viewport_width = viewport_width;
		if (this.is_ready)
		{
			this.resize_canvas(this.pixel_scaling);
		}
	}

	resize_canvas(new_pixel_scaling, fix_texel_x, fix_texel_y)
	{
		console.assert(this.is_ready);
		var dpi = window.devicePixelRatio || 1;
		var viewport_width = this.viewport_width;
		var viewport_width_no_scroll = viewport_width - k_style_scroll_width;

		var canvas_viewport = document.getElementById('canvas_viewport');
		var canvas_viewport_rect = canvas_viewport.getBoundingClientRect();
		var viewport_height = px_string_to_int(canvas_viewport.style.height);
		var viewport_height_no_scroll = viewport_height - k_style_scroll_width;

		var subresource_extent = this.get_subresource_extent();
		var canvas_rendering_res_x = subresource_extent['x'];
		var canvas_rendering_res_y = subresource_extent['y'];

		if (fix_texel_x === undefined || fix_texel_y === undefined)
		{
			fix_texel_x = canvas_rendering_res_x / 2;
			fix_texel_y = canvas_rendering_res_y / 2;
		}

		// Compute the pixel coordinate of the fixed texel.
		var fix_texel_pixel_pos_x;
		var fix_texel_pixel_pos_y;
		{
			var rect = this.canvas.getBoundingClientRect();

			fix_texel_pixel_pos_x = rect.left + rect.width * fix_texel_x / canvas_rendering_res_x - canvas_viewport_rect.left;
			fix_texel_pixel_pos_y = rect.top + rect.height * fix_texel_y / canvas_rendering_res_y - canvas_viewport_rect.top;
		}

		// Compute new canvas dimensions
		var canvas_display_w = new_pixel_scaling * canvas_rendering_res_x / dpi;
		var canvas_display_h = new_pixel_scaling * canvas_rendering_res_y / dpi;
		if (new_pixel_scaling == 0)
		{
			canvas_display_w = viewport_width_no_scroll;
			canvas_display_h = viewport_width_no_scroll * canvas_rendering_res_y / canvas_rendering_res_x;
		}

		// Compute new canvas scroll such that the fixed texel ends up at the same pixel coordinate.
		var canvas_scroll_x;
		var canvas_scroll_y;
		{
			canvas_scroll_x = fix_texel_x * canvas_display_w / canvas_rendering_res_x - fix_texel_pixel_pos_x;
			canvas_scroll_y = fix_texel_y * canvas_display_h / canvas_rendering_res_y - fix_texel_pixel_pos_y;
		}

		this.canvas.style.width = `${canvas_display_w}px`;
		this.canvas.style.height = `${canvas_display_h}px`;
		canvas_viewport.scrollLeft = canvas_scroll_x;
		canvas_viewport.scrollTop = canvas_scroll_y;
		this.pixel_scaling = new_pixel_scaling;
	}

	get_zoom_settings()
	{
		var subresource_extent = this.get_subresource_extent();

		var canvas_viewport = document.getElementById('canvas_viewport');

		var zoom_settings = {
			'subresource_extent_x': subresource_extent['x'],
			'subresource_extent_y': subresource_extent['y'],
			'pixel_scaling': this.pixel_scaling,
			'canvas_width': this.canvas.style.width,
			'canvas_height': this.canvas.style.height,
			'canvas_viewport_scoll_left': canvas_viewport.scrollLeft,
			'canvas_viewport_scoll_top': canvas_viewport.scrollTop,
		};
		return zoom_settings;
	}

	is_zoom_settings_compatible(zoom_settings)
	{
		var subresource_extent = this.get_subresource_extent();

		return (zoom_settings['subresource_extent_x'] == subresource_extent['x'] && zoom_settings['subresource_extent_y'] == subresource_extent['y']);
	}

	apply_zoom_settings(zoom_settings)
	{
		var canvas_viewport = document.getElementById('canvas_viewport');

		this.pixel_scaling = zoom_settings['pixel_scaling'];
		this.canvas.style.width = zoom_settings['canvas_width'];
		this.canvas.style.height = zoom_settings['canvas_height'];
		canvas_viewport.scrollLeft = zoom_settings['canvas_viewport_scoll_left'];
		canvas_viewport.scrollTop = zoom_settings['canvas_viewport_scoll_top'];

		this.update_change_pixel_scaling_button();
	}

	init_gl()
	{
		var gl_ctx_attributes = {
			antialias: false,
  			depth: false
		};

		// Init WebGL 2.0
		this.canvas = document.getElementById('texture_visualization_canvas');

		var gl_ctx_attributes = {
			alpha: false,
  			// premultipliedAlpha: true, // TODO
			antialias: false,
  			depth: false,
  			stencil: false,
  			powerPreference: "low-power",
  			preserveDrawingBuffer: true,
  			xrCompatible: false,
		};

		var gl = this.canvas.getContext('webgl2', gl_ctx_attributes);
		this.gl = gl;

		// Init WebGL extensions
		{
			var available_extensions = this.gl.getSupportedExtensions();
			var required_extensions = ['EXT_texture_norm16'];

			this.gl_ext = {};

			var gl = this.gl;
			var gl_ext = this.gl_ext;
			required_extensions.forEach(function(ext_name)
			{
				gl_ext[ext_name] = gl.getExtension(ext_name);
			});
		}

		// Set unpacking alignement to 1 to allow uploading texture size not multple of 4 
		gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);

		// Create buffers
		{
			this.gl_vertex_buffer = gl_create_vertex_buffer(gl, new Float32Array([
				-1.0,+1.0,0.0,
				-1.0,-1.0,0.0,
				+1.0,-1.0,0.0,
				+1.0,+1.0,0.0, 
			]));

			this.gl_index_buffer = gl_create_index_buffer(gl, new Uint16Array([0, 1, 2, 2, 0, 3]));
		}

		// Create simple shader program for NaN display mode
		{
			var frag_code = `
uniform sampler2D texture0;

in vec2 uv;
out vec4 display;

void main(void) {
	ivec2 texel_coord = ivec2(uv * vec2(textureSize(texture0, 0)));
	display = texelFetch(texture0, texel_coord, 0);
}`;
			this.gl_simple_program = this.compile_screen_shader_program(frag_code);
		}

		// Create simple shader program for NaN display mode
		{
			var frag_code = `
uniform sampler2D texture0;

in vec2 uv;
out vec4 display;

void main(void)
{
	ivec2 texel_coord = ivec2(uv * vec2(textureSize(texture0, 0)));
	vec4 raw = texelFetch(texture0, texel_coord, 0);

	display.r = (isnan(raw.r) || isnan(raw.g) || isnan(raw.b) || isnan(raw.a)) ? 1.0 : 0.0;
	display.g = 0.0;
	display.b = 0.0;
	display.a = 1.0;
}`;
			this.gl_nan_program = this.compile_screen_shader_program(frag_code);
		}

		// Create simple shader program for Inf display mode
		{
			var frag_code = `
uniform sampler2D texture0;

in vec2 uv;
out vec4 display;

void main(void)
{
	ivec2 texel_coord = ivec2(uv * vec2(textureSize(texture0, 0)));
	vec4 raw = texelFetch(texture0, texel_coord, 0);

	display.r = (isinf(raw.r) || isinf(raw.g) || isinf(raw.b) || isinf(raw.a)) ? 1.0 : 0.0;
	display.g = 0.0;
	display.b = 0.0;
	display.a = 1.0;
}`;
			this.gl_inf_program = this.compile_screen_shader_program(frag_code);
		}
	}

	release_gl()
	{
		this.canvas = null;
		this.gl = null;
		this.gl_ext = null;
		this.gl_vertex_buffer = null;
		this.gl_index_buffer = null;
		this.gl_simple_program = null;
		this.gl_nan_program = null;
		this.gl_textures = null;
	}

	release()
	{
		this.is_ready = false;
		this.raw_texture_data = null;
		this.release_gl();
		this.set_select_viewport(false);
	}

	shader_user_code_change()
	{
		if (!this.is_ready)
		{
			return;
		}
		
		this.refresh_texture_visualization();
	}

	copy_canvas_to_clipboard()
	{
		if (!this.is_ready)
		{
			return;
		}
		
		var pass_data = g_view.pass_data;

		var is_output_resource = is_pass_output_resource(pass_data, this.subresource_version_info);

		var text = '';
		text += `Dump:\n\t${g_infos['Project']} - ${g_infos['Platform']} - ${g_infos['RHI']} - ${g_infos['BuildVersion']} - ${g_infos['DumpTime']}\n\t${get_current_navigation()}\n`
		text += `Pass:\n\t${pass_data['EventName']}\n`;

		{
			var name = prettify_subresource_unique_name(this.subresource_version_info, this.resource_desc);
			text += `${is_output_resource ? 'Output' : 'Input'} resource:\n\t${name}\n`;
			if (this.display_mode == 'Visualization')
			{
				text += `\tDisplay shader: \n${g_shader_code_dict[this.shader_code_saving_key]}\n`;
				text += `\tDisplay channels: ${this.src_channels}\n`;
				text += `\tDisplay src color: ${this.src_color_space}\n`;
			}
			else
			{
				text += `\tSrc color: ${this.display_mode}\n`;
			}
		}

		if (this.subresource_version_info['draw'] >= 0)
		{
			text += `Draw ${this.subresource_version_info['draw']}:\n\t${g_view.pass_draws_data[this.subresource_version_info['draw']]['DrawName']}\n`;
		}

		copy_canvas_to_clipboard(this.canvas, text);
	}

	get shader_code_saving_key()
	{
		var name = '';
		if (this.resource_desc['Name'])
		{
			name = this.resource_desc['Name'];
		}
		else
		{
			name = this.subresource_version_info['resource'];
		}

		name = `${name}.${this.resource_desc['Format']}`;

		if (this.subresource_version_info['subresource'] && !this.subresource_version_info['subresource'].startsWith('mip'))
		{
			name = `${name}.${this.subresource_version_info['subresource']}`;
		}

		return name;
	}

	get_default_texture_shader_code()
	{
		if (this.shader_code_saving_key in g_shader_code_dict)
		{
			return g_shader_code_dict[this.shader_code_saving_key];
		}

		if (!this.subresource_desc)
		{
			return `// Sorry but ${this.resource_desc['Format']} is not supported for display :(`;
		}

		return this.subresource_desc['webgl_pixel_shader'];
	}

	translate_subresource_desc()
	{
		var gl = this.gl;
		var ue_pixel_format = this.resource_desc['Format'];

		// Missing formats:
		//PF_R8G8B8A8_SNORM
		//PF_R16G16B16A16_UNORM
		//PF_R16G16B16A16_SNORN

		if (this.resource_desc['NumSamples'] > 1)
		{
			this.error(`MSAA is not supported for visualization.`);
			return null;
		}

		var webgl_texture_internal_format = null;
		var webgl_texture_src_format = null;
		var webgl_texture_src_type = null;
		var webgl_texture_count = 1;
		var raw_channel_swizzling = [0, 1, 2, 3];
		var raw_channel_bit_depth = 0;
		var raw_channel_count = 0;
		var raw_channel_format = 'exotic';

		// 32bit floats
		if (ue_pixel_format === 'PF_A32B32G32R32F')
		{
			webgl_texture_internal_format = gl.RGBA32F;
			webgl_texture_src_format = gl.RGBA;
			webgl_texture_src_type = gl.FLOAT;
			raw_channel_bit_depth = 32;
			raw_channel_count = 4;
			raw_channel_format = 'float';
		}
		else if (ue_pixel_format === 'PF_G32R32F')
		{
			webgl_texture_internal_format = gl.RG32F;
			webgl_texture_src_format = gl.RG;
			webgl_texture_src_type = gl.FLOAT;
			raw_channel_bit_depth = 32;
			raw_channel_count = 2;
			raw_channel_format = 'float';
		}
		else if (ue_pixel_format === 'PF_R32_FLOAT' || ue_pixel_format === 'PF_BC4')
		{
			webgl_texture_internal_format = gl.R32F;
			webgl_texture_src_format = gl.RED;
			webgl_texture_src_type = gl.FLOAT;
			raw_channel_bit_depth = 32;
			raw_channel_count = 1;
			raw_channel_format = 'float';
		}
		// 16bit floats
		else if (ue_pixel_format === 'PF_FloatRGBA' || ue_pixel_format === 'PF_BC6H' || ue_pixel_format === 'PF_BC7')
		{
			webgl_texture_internal_format = gl.RGBA16F;
			webgl_texture_src_format = gl.RGBA;
			webgl_texture_src_type = gl.HALF_FLOAT;
			raw_channel_bit_depth = 16;
			raw_channel_count = 4;
			raw_channel_format = 'float';
		}
		else if (ue_pixel_format === 'PF_G16R16F' || ue_pixel_format === 'PF_G16R16F_FILTER' || ue_pixel_format === 'PF_BC5')
		{
			webgl_texture_internal_format = gl.RG16F;
			webgl_texture_src_format = gl.RG;
			webgl_texture_src_type = gl.HALF_FLOAT;
			raw_channel_bit_depth = 16;
			raw_channel_count = 2;
			raw_channel_format = 'float';
		}
		else if (ue_pixel_format === 'PF_R16F' || ue_pixel_format === 'PF_R16F_FILTER')
		{
			webgl_texture_internal_format = gl.R16F;
			webgl_texture_src_format = gl.RED;
			webgl_texture_src_type = gl.HALF_FLOAT;
			raw_channel_bit_depth = 16;
			raw_channel_count = 1;
			raw_channel_format = 'float';
		}
		// 16bit unorms
		else if (ue_pixel_format === 'PF_A16B16G16R16')
		{
			webgl_texture_internal_format = this.gl_ext['EXT_texture_norm16'].RGBA16_EXT;
			webgl_texture_src_format = gl.RGBA;
			webgl_texture_src_type = gl.UNSIGNED_SHORT;
			raw_channel_bit_depth = 16;
			raw_channel_count = 4;
			raw_channel_format = 'unorm';
		}
		else if (ue_pixel_format === 'PF_G16R16')
		{
			webgl_texture_internal_format = this.gl_ext['EXT_texture_norm16'].RG16_EXT;
			webgl_texture_src_format = gl.RG;
			webgl_texture_src_type = gl.UNSIGNED_SHORT;
			raw_channel_bit_depth = 16;
			raw_channel_count = 2;
			raw_channel_format = 'unorm';
		}
		else if (ue_pixel_format === 'PF_G16')
		{
			webgl_texture_internal_format = this.gl_ext['EXT_texture_norm16'].R16_EXT;
			webgl_texture_src_format = gl.RED;
			webgl_texture_src_type = gl.UNSIGNED_SHORT;
			raw_channel_bit_depth = 16;
			raw_channel_count = 1;
			raw_channel_format = 'unorm';
		}
		// 8bit unorms
		else if (ue_pixel_format === 'PF_B8G8R8A8' || ue_pixel_format === 'PF_R8G8B8A8')
		{
			webgl_texture_internal_format = gl.RGBA;
			webgl_texture_src_format = gl.RGBA;
			webgl_texture_src_type = gl.UNSIGNED_BYTE;
			raw_channel_bit_depth = 8;
			raw_channel_count = 4;
			raw_channel_format = 'unorm';

			if (ue_pixel_format === 'PF_B8G8R8A8' && g_infos['RHI'] !== 'OpenGL')
			{
				raw_channel_swizzling = [2, 1, 0, 3];
			}
		}
		else if (ue_pixel_format === 'PF_R8G8')
		{
			webgl_texture_internal_format = gl.RG8;
			webgl_texture_src_format = gl.RG;
			webgl_texture_src_type = gl.UNSIGNED_BYTE;
			raw_channel_bit_depth = 8;
			raw_channel_count = 2;
			raw_channel_format = 'unorm';
		}
		else if (ue_pixel_format === 'PF_R8' || ue_pixel_format === 'PF_G8' || ue_pixel_format === 'PF_A8' || ue_pixel_format === 'PF_L8')
		{
			webgl_texture_internal_format = gl.R8;
			webgl_texture_src_format = gl.RED;
			webgl_texture_src_type = gl.UNSIGNED_BYTE;
			raw_channel_bit_depth = 8;
			raw_channel_count = 1;
			raw_channel_format = 'unorm';
		}
		// 32bit uint
		else if (ue_pixel_format === 'PF_R32_UINT' || ue_pixel_format === 'PF_R32_SINT')
		{
			webgl_texture_internal_format = gl.R8UI;
			webgl_texture_src_format = gl.RED_INTEGER;
			webgl_texture_src_type = gl.UNSIGNED_BYTE;
			webgl_texture_count = 4;
			raw_channel_bit_depth = 32;
			raw_channel_count = 1;
			raw_channel_format = 'uint';
		}
		else if (ue_pixel_format === 'PF_R32G32B32A32_UINT')
		{
			webgl_texture_internal_format = gl.RGBA8UI;
			webgl_texture_src_format = gl.RGBA_INTEGER;
			webgl_texture_src_type = gl.UNSIGNED_BYTE;
			webgl_texture_count = 4;
			raw_channel_bit_depth = 32;
			raw_channel_count = 4;
			raw_channel_format = 'uint';
		}
		else if (ue_pixel_format === 'PF_R32G32_UINT')
		{
			webgl_texture_internal_format = gl.RGBA8UI;
			webgl_texture_src_format = gl.RGBA_INTEGER;
			webgl_texture_src_type = gl.UNSIGNED_BYTE;
			webgl_texture_count = 4;
			raw_channel_bit_depth = 32;
			raw_channel_count = 2;
			raw_channel_format = 'uint';
		}
		// 16bit uint
		else if (ue_pixel_format === 'PF_R16_UINT' || ue_pixel_format === 'PF_R16_SINT')
		{
			webgl_texture_internal_format = gl.R8UI;
			webgl_texture_src_format = gl.RED_INTEGER;
			webgl_texture_src_type = gl.UNSIGNED_BYTE;
			webgl_texture_count = 2;
			raw_channel_bit_depth = 16;
			raw_channel_count = 1;
			raw_channel_format = 'uint';
		}
		else if (ue_pixel_format === 'PF_R16G16B16A16_UINT' || ue_pixel_format === 'PF_R16G16B16A16_SINT')
		{
			webgl_texture_internal_format = gl.RGBA8UI;
			webgl_texture_src_format = gl.RGBA_INTEGER;
			webgl_texture_src_type = gl.UNSIGNED_BYTE;
			webgl_texture_count = 2;
			raw_channel_bit_depth = 16;
			raw_channel_count = 4;
			raw_channel_format = 'uint';
		}
		else if (ue_pixel_format === 'PF_R16G16_UINT')
		{
			webgl_texture_internal_format = gl.RGBA8UI;
			webgl_texture_src_format = gl.RGBA_INTEGER;
			webgl_texture_src_type = gl.UNSIGNED_BYTE;
			webgl_texture_count = 2;
			raw_channel_bit_depth = 16;
			raw_channel_count = 2;
			raw_channel_format = 'uint';
		}
		// 8bit uint
		else if (ue_pixel_format === 'PF_R8G8B8A8_UINT')
		{
			webgl_texture_internal_format = gl.RGBA8UI;
			webgl_texture_src_format = gl.RGBA_INTEGER;
			webgl_texture_src_type = gl.UNSIGNED_BYTE;
			raw_channel_bit_depth = 8;
			raw_channel_count = 4;
			raw_channel_format = 'uint';
		}
		else if (ue_pixel_format === 'PF_R8_UINT')
		{
			webgl_texture_internal_format = gl.R8UI;
			webgl_texture_src_format = gl.RED_INTEGER;
			webgl_texture_src_type = gl.UNSIGNED_BYTE;
			raw_channel_bit_depth = 8;
			raw_channel_count = 1;
			raw_channel_format = 'uint';
		}
		// exotic bit depth
		else if (ue_pixel_format === 'PF_FloatRGB' || ue_pixel_format === 'PF_FloatR11G11B10')
		{
			webgl_texture_internal_format = gl.R11F_G11F_B10F;
			webgl_texture_src_format = gl.RGB;
			webgl_texture_src_type = gl.UNSIGNED_INT_10F_11F_11F_REV;
			raw_channel_bit_depth = k_exotic_raw_channel_bit_depth;
			raw_channel_count = 3;
			raw_channel_format = 'float';
		}
		else if (ue_pixel_format === 'PF_A2B10G10R10')
		{
			webgl_texture_internal_format = gl.RGB10_A2;
			webgl_texture_src_format = gl.RGBA;
			webgl_texture_src_type = gl.UNSIGNED_INT_2_10_10_10_REV;
			raw_channel_bit_depth = k_exotic_raw_channel_bit_depth;
			raw_channel_count = 4;
			raw_channel_format = 'unorm';
		}
		else if (ue_pixel_format === 'PF_R5G6B5_UNORM')
		{
			webgl_texture_internal_format = gl.RGB565;
			webgl_texture_src_format = gl.RGB;
			webgl_texture_src_type = gl.UNSIGNED_SHORT_5_6_5;
			raw_channel_bit_depth = k_exotic_raw_channel_bit_depth;
			raw_channel_count = 3;
			raw_channel_format = 'unorm';
		}
		// depth stencil
		else if (ue_pixel_format === 'PF_DepthStencil' || ue_pixel_format === 'PF_ShadowDepth' || ue_pixel_format === 'PF_D24')
		{
			if (this.subresource_version_info['subresource'] == 'stencil')
			{
				webgl_texture_internal_format = gl.R8UI;
				webgl_texture_src_format = gl.RED_INTEGER;
				webgl_texture_src_type = gl.UNSIGNED_BYTE;
				raw_channel_bit_depth = 8;
				raw_channel_count = 1;
				raw_channel_format = 'uint';
			}
			else
			{
				webgl_texture_internal_format = gl.R32F;
				webgl_texture_src_format = gl.RED;
				webgl_texture_src_type = gl.FLOAT;
				raw_channel_bit_depth = 32;
				raw_channel_count = 1;
				raw_channel_format = 'float';
			}
		}
		else
		{
			this.error(`Pixel format ${ue_pixel_format} is not supported for visualization.`);
			return null;
		}

		var shader_float_vector = [null, 'float', 'vec2', 'vec3', 'vec4'];

		var shader_sampler_type = 'sampler2D';
		var shader_sampler_return_type = shader_float_vector;
		var shader_display_operation = 'texel';
		if (webgl_texture_src_format === gl.RED_INTEGER || webgl_texture_src_format === gl.RG_INTEGER || webgl_texture_src_format === gl.RGBA_INTEGER)
		{
			var divide = (BigInt(1) << BigInt(raw_channel_bit_depth)) - 1n;
			shader_sampler_type = 'usampler2D';
			shader_sampler_return_type = [null, 'uint', 'uvec2', 'uvec3', 'uvec4'];
			shader_display_operation = `${shader_float_vector[raw_channel_count]}(${shader_display_operation}) / ${divide}.0`;
		}

		var webgl_channel_count = 0;
		if (webgl_texture_src_format === gl.RED || webgl_texture_src_format === gl.RED_INTEGER)
		{
			webgl_channel_count = 1;
		}
		else if (webgl_texture_src_format === gl.RG)
		{
			webgl_channel_count = 2;
		}
		else if (webgl_texture_src_format === gl.RGB)
		{
			webgl_channel_count = 3;
		}
		else if (webgl_texture_src_format === gl.RGBA || webgl_texture_src_format === gl.RGBA_INTEGER)
		{
			webgl_channel_count = 4;
		}

		var webgl_pixel_shader_private = ``;

		var shader_fetch_operation = '(\n';
		for (var i = 0; i < webgl_texture_count; i++)
		{
			webgl_pixel_shader_private += `uniform ${shader_sampler_type} texture${i};\n`;

			shader_fetch_operation += `\t\t(texelFetch(texture${i}, texel_coord, 0) << ${8 * i})`;
			if (i + 1 == webgl_texture_count)
			{
				shader_fetch_operation += `)`;
			}
			else
			{
				shader_fetch_operation += ` |\n`;
			}
		}

		if (webgl_texture_count == 1)
		{
			shader_fetch_operation = `texelFetch(texture0, texel_coord , 0)`;
		}

		const k_get_n_channels = new Array(
			'',
			'.r',
			'.rg',
			'.rgb',
			'.rgba'
		);

		var shader_display_assignment = `display${k_get_n_channels[raw_channel_count]}`;

		webgl_pixel_shader_private += `
${shader_sampler_return_type[raw_channel_count]} fetchTexel(vec2 uv)
{
	ivec2 texel_coord = ivec2(uv * vec2(textureSize(texture0, 0)));
	${shader_sampler_return_type[4]} raw = ${shader_fetch_operation};
	return raw${k_get_n_channels[raw_channel_count]};
}

in vec2 uv;
out vec4 display;
`;

		var webgl_pixel_shader_public = `// WebGL 2.0 visualization shader for a ${ue_pixel_format} ${this.subresource_version_info['subresource']}

${shader_sampler_return_type[raw_channel_count]} texel = fetchTexel(uv);
${shader_display_assignment} = ${shader_display_operation};`;

		var gl_format = {};
		gl_format['webgl_internal_format'] = webgl_texture_internal_format;
		gl_format['webgl_src_format'] = webgl_texture_src_format;
		gl_format['webgl_src_type'] = webgl_texture_src_type;
		gl_format['webgl_texture_count'] = webgl_texture_count;
		gl_format['webgl_pixel_shader_public'] = webgl_pixel_shader_public;
		gl_format['webgl_pixel_shader_private'] = webgl_pixel_shader_private;
		gl_format['webgl_channel_count'] = webgl_channel_count;
		gl_format['raw_channel_swizzling'] = raw_channel_swizzling;
		gl_format['raw_channel_bit_depth'] = raw_channel_bit_depth;
		gl_format['raw_channel_count'] = raw_channel_count;
		gl_format['raw_channel_format'] = raw_channel_format;
		gl_format['ue_pixel_format'] = ue_pixel_format;

		return gl_format;
	}

	change_pixel_scaling(pixel_scaling_button)
	{
		if (!this.is_ready)
		{
			return;
		}
		
		const pixel_scalings = {
			'Fit': 0,
			'1:1': 1,
			'2:1': 2,
			'4:1': 4,
			'8:1': 8,
			'16:1': 16,
		};
		this.resize_canvas(pixel_scalings[pixel_scaling_button.value]);
		update_value_selection(document.getElementById('texture_visualization_change_pixel_scaling'), pixel_scaling_button.value);
	}

	change_src_channels(new_src_channels)
	{
		if (!this.is_ready)
		{
			return;
		}
		
		if (new_src_channels.value == this.src_channels)
		{
			return;
		}

		this.src_channels = new_src_channels.value;

		update_value_selection(document.getElementById('texture_visualization_change_src_channels'), this.src_channels);

		this.refresh_texture_visualization();
	}

	change_src_color(new_src_color_space)
	{
		if (!this.is_ready)
		{
			return;
		}
		
		if (new_src_color_space.value == this.src_color_space)
		{
			return;
		}

		this.src_color_space = new_src_color_space.value;

		update_value_selection(document.getElementById('texture_visualization_change_src_color'), this.src_color_space);

		this.refresh_texture_visualization();
	}

	change_display_mode(new_mode)
	{
		if (!this.is_ready)
		{
			return;
		}
		
		if (new_mode.value == this.display_mode)
		{
			return;
		}

		this.display_mode = new_mode.value;

		update_value_selection(document.getElementById('texture_visualization_change_display_modes'), this.display_mode);

		if (this.display_mode == 'Visualization')
		{
			document.getElementById('texture_visualization_code_input').readOnly = false;
		}
		else
		{
			document.getElementById('texture_visualization_code_input').readOnly = true;
		}

		this.refresh_texture_visualization();
	}

	get_subresource_extent()
	{
		var extent = {};
		extent['x'] = this.resource_desc['ExtentX'];
		extent['y'] = this.resource_desc['ExtentY'];

		if (this.subresource_version_info['subresource'].startsWith('mip'))
		{
			var mip_id = Number(this.subresource_version_info['subresource'].substring(3));

			for (var i = 0; i < mip_id; i++)
			{
				extent['x'] = Math.floor(extent['x'] / 2);
				extent['y'] = Math.floor(extent['y'] / 2);
			}
		}

		return extent;
	}

	create_gl_texture(webgl_internal_format, webgl_src_format, webgl_src_type, extent, converted_data)
	{
		console.assert(this.is_ready);
		var gl = this.gl;

		var texture = gl.createTexture();
		gl.activeTexture(gl.TEXTURE0);
		gl.bindTexture(gl.TEXTURE_2D, texture);
		gl.texImage2D(
			gl.TEXTURE_2D,
			/* level = */ 0,
			webgl_internal_format,
		    extent['x'],
		    extent['y'],
		    /* border = */ 0,
		    webgl_src_format,
		    webgl_src_type,
		    converted_data);
		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
		gl.bindTexture(gl.TEXTURE_2D, null);

		return texture;
	}

	process_texture_data_for_visualization()
	{
		console.assert(this.is_ready);
		var gl = this.gl;

		if (this.raw_texture_data === null)
		{
			console.error(`failed to load ${get_subresource_unique_version_name(this.subresource_version_info)}`);
			return null;
		}

		var extent = this.get_subresource_extent();
		var webgl_array_size = this.subresource_desc['webgl_channel_count'] * extent['x'] * extent['y'];

		// Reset the gl textures
		this.gl_textures = [];

		// Exotic pixel format
		if (this.subresource_desc['raw_channel_bit_depth'] == k_exotic_raw_channel_bit_depth)
		{
			var gl = this.gl;
			var original_converted_data = null;
			if (this.subresource_desc['webgl_src_type'] === gl.UNSIGNED_SHORT_5_6_5)
			{
				original_converted_data = new Uint16Array(this.raw_texture_data);
			}
			else if (this.subresource_desc['webgl_src_type'] === gl.UNSIGNED_INT_10F_11F_11F_REV || this.subresource_desc['webgl_src_type'] === gl.UNSIGNED_INT_2_10_10_10_REV)
			{
				original_converted_data = new Uint32Array(this.raw_texture_data);
			}
			else
			{
				return null;
			}

			var texture = this.create_gl_texture(this.subresource_desc['webgl_internal_format'], this.subresource_desc['webgl_src_format'], this.subresource_desc['webgl_src_type'], extent, original_converted_data);
			this.gl_textures.push(texture);
		}
		else if (this.subresource_desc['webgl_texture_count'] > 1)
		{
			var original_converted_data = null;
			if (this.subresource_desc['raw_channel_bit_depth'] === 8)
			{
				original_converted_data = new Uint8Array(this.raw_texture_data);
			}
			else if (this.subresource_desc['raw_channel_bit_depth'] === 16)
			{
				original_converted_data = new Uint16Array(this.raw_texture_data);
			}
			else if (this.subresource_desc['raw_channel_bit_depth'] === 32)
			{
				original_converted_data = new Uint32Array(this.raw_texture_data);
			}
			else
			{
				return null;
			}

			for (var tex_id = 0; tex_id < this.subresource_desc['webgl_texture_count']; tex_id++)
			{
				var converted_data = new Uint8Array(webgl_array_size);

				for (var y = 0; y < extent['y']; y++)
				{
					for (var x = 0; x < extent['x']; x++)
					{
						for (var c = 0; c < this.subresource_desc['webgl_channel_count']; c++)
						{
							var texel_offset = x + extent['x'] * y;

							var value = 0;
							if (c < this.subresource_desc['raw_channel_count'])
							{
								var src = original_converted_data[texel_offset * this.subresource_desc['raw_channel_count'] + this.subresource_desc['raw_channel_swizzling'][c]];
								value = (src >> (8 * tex_id)) % 256;
							}
							converted_data[texel_offset * this.subresource_desc['webgl_channel_count'] + c] = value;
						}
					}
				}

				var texture = this.create_gl_texture(this.subresource_desc['webgl_internal_format'], this.subresource_desc['webgl_src_format'], this.subresource_desc['webgl_src_type'], extent, converted_data);
				this.gl_textures.push(texture);
			}
		}
		else
		{
			var gl = this.gl;
			var converted_data = null;
			var original_converted_data = null;
			if (this.subresource_desc['webgl_src_type'] === gl.UNSIGNED_BYTE)
			{
				original_converted_data = new Uint8Array(this.raw_texture_data);
				converted_data = new Uint8Array(webgl_array_size);
			}
			else if (this.subresource_desc['webgl_src_type'] === gl.UNSIGNED_SHORT || this.subresource_desc['webgl_src_type'] === gl.HALF_FLOAT)
			{
				original_converted_data = new Uint16Array(this.raw_texture_data);
				converted_data = new Uint16Array(webgl_array_size);
			}
			else if (this.subresource_desc['webgl_src_type'] === gl.FLOAT)
			{
				original_converted_data = new Float32Array(this.raw_texture_data);
				converted_data = new Float32Array(webgl_array_size);
			}
			else
			{
				return null;
			}

			for (var y = 0; y < extent['y']; y++)
			{
				for (var x = 0; x < extent['x']; x++)
				{
					for (var c = 0; c < this.subresource_desc['webgl_channel_count']; c++)
					{
						var i = this.subresource_desc['webgl_channel_count'] * (x + extent['x'] * y);

						var value = 0;
						if (c < this.subresource_desc['raw_channel_count'])
						{
							value = original_converted_data[i + this.subresource_desc['raw_channel_swizzling'][c]];
						}
						converted_data[i + c] = value;
					}
				}
			}

			var texture = this.create_gl_texture(this.subresource_desc['webgl_internal_format'], this.subresource_desc['webgl_src_format'], this.subresource_desc['webgl_src_type'], extent, converted_data);
			this.gl_textures.push(texture);
		}
	}

	compile_screen_shader_program(frag_code)
	{
		var vert_code = `
in vec3 coordinates;
out highp vec2 uv;
void main(void) {
	gl_Position = vec4(coordinates, 1.0);
	uv = coordinates.xy * 0.5 + 0.5;
}`;

		var shader_program = gl_create_shader_program(this.gl, vert_code, frag_code);
		if (typeof shader_program === 'string')
		{
			document.getElementById('texture_visualization_code_log').value = 'Compilation failed:\n' + shader_program;
			return null;
		}
		else
		{
			document.getElementById('texture_visualization_code_log').value = 'Compilation succeeded';
		}
		return shader_program;
	}

	refresh_texture_visualization()
	{
		console.assert(this.is_ready);
		console.assert(this.gl_textures);

		var shader_program = null;
		if (this.display_mode == 'NaN')
		{
			shader_program = this.gl_nan_program;
		}
		else if (this.display_mode == 'Inf')
		{
			shader_program = this.gl_inf_program;
		}
		else
		{
			var user_frag_code = document.getElementById('texture_visualization_code_input').value;

			var final_frag_code = this.subresource_desc['webgl_pixel_shader_private'] + 'void main(void) { display = vec4(0.0, 0.0, 0.0, 0.0);\n';
			final_frag_code += user_frag_code;
			final_frag_code += '\n';

			if (this.src_channels == 'R')
			{
				final_frag_code += 'display.rgb = vec3(display.r, 0.0, 0.0);';
			}
			else if (this.src_channels == 'G')
			{
				final_frag_code += 'display.rgb = vec3(0.0, display.g, 0.0);';
			}
			else if (this.src_channels == 'B')
			{
				final_frag_code += 'display.rgb = vec3(0.0, 0.0, display.b);';
			}
			else if (this.src_channels == 'A')
			{
				final_frag_code += 'display.rgb = display.aaa;';
			}

			if (this.src_color_space == 'sRGB/Rec709')
			{
				// NOP because src gamma = webgl display gamma.
			}
			else if (this.src_color_space == 'RGB Linear[0;1]')
			{
				// Apply webgl's 2.2 gamma curve on the display color so linear gradients look linear in web browser.
				final_frag_code += 'display.rgb = pow(display.rgb, vec3(2.2));';
			}
			final_frag_code += '}';

			shader_program = this.compile_screen_shader_program(final_frag_code);

			if (!shader_program)
			{
				return;
			}

			// Save the user code if compilation have succeeded.
			g_shader_code_dict[this.shader_code_saving_key] = user_frag_code;
		}

		var gl = this.gl;
		gl.clearColor(0.0, 0.0, 0.0, 1.0);
		gl.clear(gl.COLOR_BUFFER_BIT);

		{
			gl.useProgram(shader_program);
			gl.bindBuffer(gl.ARRAY_BUFFER, this.gl_vertex_buffer);
			gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.gl_index_buffer);

			var coord = gl.getAttribLocation(shader_program, "coordinates");
			gl.vertexAttribPointer(coord, 3, gl.FLOAT, false, 0, 0); 
			gl.enableVertexAttribArray(coord);

			for (var tex_id = 0; tex_id < this.gl_textures.length; tex_id++)
			{
				gl.activeTexture(gl.TEXTURE0 + tex_id);
				gl.bindTexture(gl.TEXTURE_2D, this.gl_textures[tex_id]);

				var texture_sampler = gl.getUniformLocation(shader_program, 'texture' + tex_id);
				gl.uniform1i(texture_sampler, tex_id);
			}

			gl.viewport(0, 0, this.canvas.width, this.canvas.height);
			gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
		}
	}
} // class TextureView

function display_texture(subresource_version_info)
{
	if (g_view.resource_view !== null && get_subresource_unique_version_name(g_view.resource_view.subresource_version_info) == get_subresource_unique_version_name(subresource_version_info))
	{
		return;
	}

	var resource_desc = get_resource_desc(subresource_version_info['resource']);
	g_view.set_resource_view(new TextureView(subresource_version_info, resource_desc));
}


// -------------------------------------------------------------------- DISPLAY BUFFER

const k_buffer_pixel_per_row = 20;
const k_buffer_max_display_row_count = 50;
const k_buffer_display_row_buffer = 50;


class GenericBufferView extends ResourceView
{
	constructor(subresource_version_info, resource_desc)
	{
		super(subresource_version_info, resource_desc);
		this.raw_buffer_data = null;
		this.raw_buffer_data_path = null;
		this.viewport_selected = false;
		this.display_row_start = 0;
		this.selected_address = -1;
		this.structure_metadata = null;
		this.display_elem_using_rows = resource_desc['NumElements'] == 1;
	}

	setup_html(parent_dom)
	{
		var html = '';

		if (this.resource_desc['Desc'] != 'FRDGParameterStruct')
		{
			var resource_info_htmls = `
				<table width="100%" class="resource_desc pretty_table">`;
			
			for (var key in this.resource_desc)
			{
			    if (this.resource_desc.hasOwnProperty(key))
			    {
			    	var value = this.resource_desc[key];

			    	if (key == 'Metadata')
			    	{
			    		if (this.resource_desc[key] == k_null_json_ptr)
			    		{
			    			value = 'nullptr';
			    		}
			    		else if (this.structure_metadata)
			    		{
			    			value = `${this.structure_metadata['StructTypeName']} (${get_filename(this.structure_metadata['FileName'])}:${this.structure_metadata['FileLine']})`;
			    		}
			    	}

					resource_info_htmls += `
						<tr>
							<td>${key}</td>
							<td>${value}</td>
						</tr>`;
			    }
			}

			resource_info_htmls += `
				</table>`;

			html += `
				<div class="main_div">
					<div class="selection_list_title">Buffer descriptor: ${this.resource_desc['Name']}</div>
					<div>
						${resource_info_htmls}
					</div>
				</div>`;
		}

		html += `
			<div class="main_div">
				<div class="selection_list_title" style="width: auto;">Buffer visualization: ${this.resource_desc['Name']}</div>
				<div id="buffer_outter">
					<div id="buffer_viewport_header">
						<table width="100%" id="buffer_visualization_format_outter">
							<tr>
								<td style="padding: 4px 20px 0 20px; text-align: right;" width="40px">Format:</td>
								<td>
									<input type="text" id="buffer_visualization_format" onchange="g_view.resource_view.refresh_buffer_visualization(true);" style="width: 100%;" />
								</td>
							</tr>
						</table>
						<div id="buffer_visualization_member_search_outter">
							<input type="search" id="buffer_visualization_member_search" oninput="g_view.resource_view.refresh_members();" onchange="g_view.resource_view.refresh_members();" style="width: 100%;" placeholder="Search member..." />
						</div>
					</div>
					<div id="buffer_viewport">
						<div id="buffer_content_pannel"></div>
					</div>
				</div>
			</div>`;

		parent_dom.innerHTML = html;

		if (this.structure_metadata)
		{
			// document.getElementById('buffer_visualization_format').value = `${this.structure_metadata['StructTypeName']} (${get_filename(this.structure_metadata['FileName'])}:${this.structure_metadata['FileLine']})`;
			// document.getElementById('buffer_visualization_format').readOnly = true;
			document.getElementById('buffer_visualization_format_outter').style.display = 'none';
		}
		else
		{
			var default_format = '';
			{
				var byte_per_element = this.resource_desc['BytesPerElement'];
				var member_count = byte_per_element / 4;

				var default_format = 'hex(uint)';

				// DrawIndirect flags means this is certainly a 32bit uint.
				if ('Usage' in this.resource_desc && this.resource_desc['Usage'].includes('DrawIndirect'))
				{
					default_format = 'uint';
				}

				var format_per_column = [];
				for (var i = 0; i < member_count; i++)
				{
					format_per_column.push(default_format);
				}

				default_format = format_per_column.join(', ');
			}

			document.getElementById('buffer_visualization_format').value = default_format;
			document.getElementById('buffer_visualization_member_search_outter').style.display = 'none';
		}

		// Setup mouse callbacks
		{
			var buffer_view = this;

			document.getElementById('buffer_outter').onclick = function(event) { buffer_view.onclick_buffer_outter(event); };
			document.getElementById('buffer_outter').onmouseleave = function(event) { buffer_view.set_select_viewport(false); };
			document.getElementById('buffer_viewport').onclick = function(event) { buffer_view.onclick_buffer_viewport(event); };
			document.getElementById('buffer_viewport').onscroll = function(event) { buffer_view.update_buffer_scroll(/* force_refresh = */ false); };
		}

		var buffer_view = this;
		console.assert(this.raw_buffer_data_path);
		load_resource_binary_file(this.raw_buffer_data_path, function(raw_buffer_data)
		{
			if (raw_buffer_data)
			{
				buffer_view.raw_buffer_data = raw_buffer_data;
				buffer_view.refresh_buffer_visualization(/* refresh header = */ true);
				buffer_view.onload();
			}
			else
			{
				document.getElementById('buffer_content_pannel').innerHTML = `
					Error: ${this.raw_buffer_data_path} couldn't be loaded.`;
			}
		});

		document.title = `${g_view.pass_data['EventName']} - ${this.resource_desc['Name']}`;
	}

	refresh_members()
	{
		this.refresh_buffer_visualization(/* refresh_header = */ !this.display_elem_using_rows);
	}

	onclick_buffer_outter(event)
	{
		this.set_select_viewport(true);

		if (this.display_elem_using_rows)
		{
			return;
		}

		var content_rect = document.getElementById('buffer_content_pannel').getBoundingClientRect();
		var header_rect = document.getElementById('buffer_content_header').getBoundingClientRect();

		var html_x = event.clientX - content_rect.left;
		var html_y = event.clientY - content_rect.top - header_rect.height;

		var new_selected_address = Math.floor(html_y / k_buffer_pixel_per_row);

		if (new_selected_address != this.selected_address && new_selected_address < this.resource_desc['NumElements'])
		{
			this.selected_address = new_selected_address;
			this.refresh_buffer_visualization();
		}
	}

	onclick_buffer_viewport(event)
	{
		this.set_select_viewport(true);
	}

	set_select_viewport(selected)
	{
		if (this.viewport_selected === selected)
		{
			return;
		}

		this.viewport_selected = selected;
		if (this.viewport_selected)
		{
			var buffer_view = this;
			document.getElementById('buffer_outter').classList.add('selected');
			document.getElementById('buffer_viewport').style.overflow = 'scroll';
			//document.getElementById('buffer_viewport').style.margin = '0px 0px 0px 0px';
		}
		else
		{
			document.getElementById('buffer_outter').classList.remove('selected');
			document.getElementById('buffer_viewport').style.overflow = 'hidden';
			//document.getElementById('buffer_viewport').style.margin = `0px ${k_scroll_width}px ${k_scroll_width}px 0px`;
		}
	}

	update_buffer_scroll(force_refresh)
	{
		var buffer_viewport = document.getElementById('buffer_viewport');

		var new_display_row_start = Math.max(2 * Math.floor((buffer_viewport.scrollTop / k_buffer_pixel_per_row) * 0.5), 0);

		if (new_display_row_start != this.display_row_start || force_refresh)
		{
			this.display_row_start = new_display_row_start;
			this.refresh_buffer_visualization(/* refresh_header = */ false);
		}
	}

	go_to_address()
	{
		var byte_per_element = this.resource_desc['BytesPerElement'];

		var address = document.getElementById('buffer_address_input').value;
		if (address == '')
		{
			if (this.selected_address != -1)
			{
				this.selected_address = -1;
				this.update_buffer_scroll(/* force_refresh = */ true);
			}
			return;
		}

		var element_id = Math.floor(Number(address) / byte_per_element);
		if (element_id < this.resource_desc['NumElements'])
		{
			this.selected_address = element_id;

			var buffer_viewport = document.getElementById('buffer_viewport');
			buffer_viewport.scrollTop = (element_id - 5) * k_buffer_pixel_per_row;

			this.update_buffer_scroll(/* force_refresh = */ true);
		}
	}

	refresh_buffer_visualization(refresh_header)
	{
		var byte_per_element = this.resource_desc['BytesPerElement'];
		var num_element = this.resource_desc['NumElements'];

		var member_count = 0;
		var member_names = [];
		var member_byte_offset = [];
		var member_format = [];
		if (this.structure_metadata)
		{
			var member_search = document.getElementById('buffer_visualization_member_search').value;

			iterate_structure_members(this.structure_metadata, function(it) {
				var column_format = undefined;
				if (it.base_type == 'UBMT_INT32')
				{
					column_format = 'int';
				}
				else if (it.base_type == 'UBMT_UINT32')
				{
					column_format = 'uint';
				}
				else if (it.base_type == 'UBMT_FLOAT32')
				{
					column_format = 'float';
				}
				else
				{
					return;
				}

				if (member_search)
				{
					if (!it.cpp_name.toLowerCase().includes(member_search.toLowerCase()))
					{
						return;
					}
				}

				const k_suffixes = ['.x', '.y', '.z', '.w'];

				var relative_offset = 0;
				for (var elem_id = 0; elem_id < Math.max(it.member['NumElements'], 1); elem_id++)
				{
					for (var row_id = 0; row_id < Math.max(it.member['NumRows'], 1); row_id++)
					{
						for (var column_id = 0; column_id < it.member['NumColumns']; column_id++)
						{
							var name = it.cpp_name;

							if (it.member['NumElements'] > 0)
							{
								name += `[${elem_id}]`;
							}
							if (it.member['NumRows'] > 1)
							{
								name += k_suffixes[row_id];
							}
							if (it.member['NumColumns'] > 1)
							{
								name += k_suffixes[column_id];
							}

							member_count += 1;
							member_byte_offset.push(it.byte_offset + relative_offset);
							member_format.push(column_format);
							member_names.push(name);

							relative_offset += 4;
						}
					}
				}
			});
		}
		else
		{
			member_count = byte_per_element / 4;

			var text_format = document.getElementById('buffer_visualization_format').value;

			var column_formats = text_format.split(',');

			var byte_offset = 0;

			column_formats.forEach(function(value, index) {
				var format_to_use = value.trim();

				member_byte_offset.push(byte_offset);
				member_format.push(format_to_use);
				member_names.push(format_to_use);

				byte_offset += 4;
			});

			for (var i = member_format.length; i < member_count; i++)
			{
				var format_to_use = member_format[i % column_formats.length];

				member_byte_offset.push(byte_offset);
				member_format.push(format_to_use);
				member_names.push(format_to_use);

				byte_offset += 4;
			}

		}

		var column_count = member_count;
		var row_count = num_element;
		if (this.display_elem_using_rows)
		{
			if (this.structure_metadata)
			{
				column_count = 2;
			}
			else
			{
				column_count = 1;
			}
			row_count = member_count;
		}

		var display_start_row_id = Math.max(this.display_row_start - k_buffer_display_row_buffer, 0);
		var display_row_count = Math.min(row_count - display_start_row_id, k_buffer_max_display_row_count + 2 * k_buffer_display_row_buffer);
		var display_end_row_id = display_start_row_id + display_row_count;

		var scroll_top = document.getElementById('buffer_viewport').scrollTop;

		if (refresh_header)
		{
			var address_search_value = '';
			if (document.getElementById('buffer_address_input'))
			{
				address_search_value = document.getElementById('buffer_address_input').value;
			}

			var classes = 'pretty_table';
			if (this.display_elem_using_rows)
			{
				classes += ' display_elem_using_rows';
			}

			var resource_content = `
				<table id="buffer_content_table" class="${classes}" width="100%">
					<tr class="header" id="buffer_content_header">`;

			if (this.display_elem_using_rows)
			{
				resource_content += `<td>Member</td><td>Value</td>`;
				if (this.structure_metadata)
				{
					resource_content += `<td>Hex</td>`;
				}
			}
			else
			{
				resource_content += `<td><input type="text" id="buffer_address_input" onchange="g_view.resource_view.go_to_address();" style="width: 100%;" placeholder="Address..." value="${address_search_value}" /></td>`;
				for (var i = 0; i < member_count; i++)
				{
					resource_content += `<td>${member_names[i]}</td>`;
				}
			}

			resource_content += `
						<td></td>
					</tr>
				</table>`;

			document.getElementById('buffer_content_pannel').innerHTML = resource_content;
		}
		else
		{
			var table_dom = document.getElementById("buffer_content_table");
			while (table_dom.lastElementChild != table_dom.firstElementChild)
			{
				table_dom.removeChild(table_dom.lastElementChild);
			}
		}

		{
			// Start padding
			var resource_content = ``;
			{
				resource_content += `
					<tr>
						<td style="height: ${k_buffer_pixel_per_row * display_start_row_id}px; padding: 0;"></td>
					</tr>`;
			}

			for (var row_id = display_start_row_id; row_id < display_end_row_id; row_id++)
			{
				var elem_id = row_id;
				if (this.display_elem_using_rows)
				{
					elem_id = 0;
				}

				var classes = '';
				if (elem_id == this.selected_address)
				{
					classes = 'highlighted';
				}

				resource_content += `
					<tr class="${classes}">`;

				if (this.display_elem_using_rows)
				{
					resource_content += `
						<td>${member_names[row_id]}</td>`;
				}
				else
				{
					resource_content += `
						<td>0x${(elem_id * byte_per_element).toString(16)}</td>`;
				}

				for (var i = 0; i < column_count; i++)
				{
					var member_id = i;
					if (this.display_elem_using_rows)
					{
						member_id = row_id;
					}

					var byte_offset = byte_per_element * elem_id + member_byte_offset[member_id];

					var value = null;
					var display = member_format[member_id];

					if (this.display_elem_using_rows && i == 1 && this.structure_metadata)
					{
						display = `hex(${display})`;
					}

					if (display == 'float')
					{
						value = decode_float32((new Uint32Array(this.raw_buffer_data, byte_offset, 1))[0]);
					}
					else if (display == 'half')
					{
						value = decode_float16((new Uint16Array(this.raw_buffer_data, byte_offset, 1))[0]);
					}
					else if (display == 'int')
					{
						value = (new Int32Array(this.raw_buffer_data, byte_offset, 1))[0];
					}
					else if (display == 'uint')
					{
						value = (new Uint32Array(this.raw_buffer_data, byte_offset, 1))[0];
					}
					else if (display == 'short')
					{
						value = (new Int16Array(this.raw_buffer_data, byte_offset, 1))[0];
					}
					else if (display == 'ushort')
					{
						value = (new Uint16Array(this.raw_buffer_data, byte_offset, 1))[0];
					}
					else if (display == 'char')
					{
						value = (new Int8Array(this.raw_buffer_data, byte_offset, 1))[0];
					}
					else if (display == 'uchar')
					{
						value = (new Uint8Array(this.raw_buffer_data, byte_offset, 1))[0];
					}
					else if (display == 'hex(int)' || display == 'hex(uint)' || display == 'hex(float)')
					{
						value = '0x' + (new Uint32Array(this.raw_buffer_data, byte_offset, 1))[0].toString(16).padStart(8, 0);
					}
					else if (display == 'hex(short)' || display == 'hex(ushort)' || display == 'hex(half)')
					{
						value = '0x' + (new Uint16Array(this.raw_buffer_data, byte_offset, 1))[0].toString(16).padStart(4, 0);
					}
					else if (display == 'hex(char)' || display == 'hex(uchar)')
					{
						value = '0x' + (new Uint8Array(this.raw_buffer_data, byte_offset, 1))[0].toString(16).padStart(2, 0);
					}
					else if (display == 'bin(int)' || display == 'bin(uint)' || display == 'bin(float)')
					{
						value = '0b' + (new Uint32Array(this.raw_buffer_data, byte_offset, 1))[0].toString(2).padStart(32, 0);
					}
					else if (display == 'bin(short)' || display == 'bin(ushort)' || display == 'bin(half)')
					{
						value = '0b' + (new Uint16Array(this.raw_buffer_data, byte_offset, 1))[0].toString(2).padStart(16, 0);
					}
					else if (display == 'bin(char)' || display == 'bin(uchar)')
					{
						value = '0b' + (new Uint8Array(this.raw_buffer_data, byte_offset, 1))[0].toString(2).padStart(8, 0);
					}
					else
					{
						value = `Unknown ${display}`;
					}

					resource_content += `<td>${value}</td>`;
				}

				resource_content += `
						<td></td>
					</tr>`;
			} // for (var row_id = display_start_row_id; row_id < display_end_row_id; row_id++)

			if (row_count == 0)
			{
				resource_content += `
					<tr>
						<td colspan="${column_count + 2}" class="empty" align="center">Empty</td>
					</tr>`;
			}

			// End padding
			{
				resource_content += `
					<tr>
						<td style="height: ${k_buffer_pixel_per_row * (row_count - display_end_row_id)}px; padding: 0;"></td>
					</tr>`;
			}

			document.getElementById('buffer_content_table').innerHTML += resource_content;
		}

		document.getElementById('buffer_viewport').scrollTop = scroll_top;

		{
			var min_row_count = 10;
			document.getElementById('buffer_viewport').style.height = `${k_style_scroll_width + Math.min(Math.max(row_count + 1, min_row_count), k_buffer_max_display_row_count) * k_buffer_pixel_per_row}px`;
		}
	}

	release()
	{
		this.raw_buffer_data = null;
	}
} // class GenericBufferView


class BufferView extends GenericBufferView
{
	constructor(subresource_version_info, resource_desc)
	{
		super(subresource_version_info, resource_desc);
		this.raw_buffer_data_path = `Resources/${get_subresource_unique_version_name(this.subresource_version_info)}.bin`;

		if (resource_desc['Metadata'] != k_null_json_ptr)
		{
			this.structure_metadata = load_structure_metadata(resource_desc['Metadata']);
		}
	}
} // class BufferView


// -------------------------------------------------------------------- PARAMETER STRUCTURE

class ParameterStructureView extends GenericBufferView
{
	constructor(subresource_version_info, resource_desc, structure_metadata)
	{
		super(subresource_version_info, resource_desc);
		this.raw_buffer_data_path = `Structures/${subresource_version_info['resource']}.bin`;
		this.structure_metadata = structure_metadata;
	}

} // class ParameterStructureView

function display_pass_parameters(pass_id)
{
	display_pass_internal(pass_id);

	var pass_data = g_passes[pass_id];
	var structure_metadata = load_structure_metadata(pass_data['ParametersMetadata']);

	var subresource_version_info = {};
	subresource_version_info['subresource'] = null;
	subresource_version_info['resource'] = pass_data['Parameters'];
	subresource_version_info['pass'] = pass_data['Pointer'];
	subresource_version_info['draw'] = -1;

	var resource_desc = {};
	resource_desc['Name'] = 'PassParameters';
	resource_desc['ByteSize'] = structure_metadata['Size'];
	resource_desc['Desc'] = 'FRDGParameterStruct';
	resource_desc['BytesPerElement'] = resource_desc['ByteSize'];
	resource_desc['NumElements'] = 1;
	resource_desc['Metadata'] = pass_data['ParametersMetadata'];
	resource_desc['Usage'] = [];

	g_view.set_resource_view(new ParameterStructureView(subresource_version_info, resource_desc, structure_metadata));
}


// -------------------------------------------------------------------- GENERIC 3D STRUCTURE VIEW

class Generic3DStructureView extends ResourceView
{
	constructor(subresource_version_info, resource_desc)
	{
		super(subresource_version_info, resource_desc);
		this.is_ready = false;
		this.release();
		this.camera_pos = [200.0, 200.0, 200.0];
		this.camera_fov = 90.0;
		this.camera_longitude = 0.0;
		this.camera_latitude = 0.0;
		this.camera_look_at([0.0, 0.0, 0.0]);
		this.camera_movement_u = 0;
		this.camera_movement_v = 0;
		this.camera_movement_z = 0;
		this.camera_movement_speed = 100.0; // 1 m/s
		this.prev_time = 0;
	}

	setup_html(parent_dom)
	{
		var resource_info_htmls = '';
		{
			resource_info_htmls += `
				<table width="100%" class="pretty_table resource_desc">`;
			
			for (var key in this.resource_desc)
			{
			    if (this.resource_desc.hasOwnProperty(key)){
					resource_info_htmls += `
						<tr>
							<td>${key}</td>
							<td>${this.resource_desc[key]}</td>
						</tr>`;
			    }
			}

			resource_info_htmls += `
				</table>`;
		}

		var title = prettify_subresource_unique_name(this.subresource_version_info, this.resource_desc);
		if (this.subresource_version_info['draw'] >= 0)
		{
			title += ` draw:${this.subresource_version_info['draw']}`;
		}

		var html = `
			<div class="main_div">
				<div class="selection_list_title">Ray-tracing Acceleration Structure visualization: ${title}</div>
				<div id="canvas_outter">
					<div id="canvas_header">
						<div class="button_list">
							<input type="button" onclick="g_view.resource_view.copy_canvas_to_clipboard();" value="Copy to clipboard"/>
						</div>
					</div>
					<canvas
						id="generic_3d_visualization_canvas"
						width="100"
						height="100"
						style="width: 100px; height: 100px;"></canvas>
				</div>
				<table width="100%">
					<tr>
						<td width="50%">
							<div class="selection_list_title">Buffer descriptor</div>
							${resource_info_htmls}
						</td>
					</tr>
				</table>
			</div>`;

		parent_dom.innerHTML = html;

		// Init WebGL 2.0
		this.init_gl();

		// Setup mouse motion callback
		{
			var generic_3d_view = this;

			this.canvas.onclick = function(event) { generic_3d_view.onclick_canvas(); };
			document.getElementById('canvas_outter').onmouseleave = function(event) { generic_3d_view.set_select_viewport(false); }
		}

		this.init();
	}

	init_gl()
	{
		var gl_ctx_attributes = {
			antialias: false,
  			depth: false
		};

		// Init WebGL 2.0
		this.canvas = document.getElementById('generic_3d_visualization_canvas');

		var gl_ctx_attributes = {
			alpha: true,
  			// premultipliedAlpha: true, // TODO
			antialias: false,
  			depth: true,
  			stencil: false,
  			powerPreference: "low-power",
  			preserveDrawingBuffer: true,
  			xrCompatible: false,
		};

		var gl = this.canvas.getContext('webgl2', gl_ctx_attributes);
		this.gl = gl;

		// Init WebGL extensions
		{
			var available_extensions = this.gl.getSupportedExtensions();
			var required_extensions = ['EXT_texture_norm16'];

			this.gl_ext = {};

			var gl = this.gl;
			var gl_ext = this.gl_ext;
			required_extensions.forEach(function(ext_name)
			{
				gl_ext[ext_name] = gl.getExtension(ext_name);
			});
		}

		// Create unit cube buffers exactly like GUnitCubeVertexBuffer and GUnitCubeIndexBuffer
		{
			var unit_cube_vertices = new Array(8 * 3);
			for (var z = 0; z < 2; z++)
			{
				for (var y = 0; y < 2; y++)
				{
					for (var x = 0; x < 2; x++)
					{
						var dest = x * 4 + y * 2 + z;
						unit_cube_vertices[dest * 3 + 0] = x ? -100.0 : 100.0;
						unit_cube_vertices[dest * 3 + 1] = y ? -100.0 : 100.0;
						unit_cube_vertices[dest * 3 + 2] = z ? -100.0 : 100.0;
					}
				}
			}

			var unit_cube_indices = [
				0, 2, 3,
				0, 3, 1,
				4, 5, 7,
				4, 7, 6,
				0, 1, 5,
				0, 5, 4,
				2, 6, 7,
				2, 7, 3,
				0, 4, 6,
				0, 6, 2,
				1, 3, 7,
				1, 7, 5,
			];

			this.gl_unit_cube = {}
			this.gl_unit_cube.vertex_buffer = gl_create_vertex_buffer(gl, new Float32Array(unit_cube_vertices));
			this.gl_unit_cube.index_buffer = gl_create_index_buffer(gl, new Uint16Array(unit_cube_indices));
			this.gl_unit_cube.index_count = unit_cube_indices.length;
		}

		// Create simple shader program
		{
			var vert_code = `
uniform mat4 local_to_world;
uniform mat4 world_to_view;
uniform mat4 view_to_clip;

in vec3 vertex_coordinates;
out vec3 interpolator_world;

void main(void) {
	vec4 world_pos = local_to_world * vec4(vertex_coordinates.xyz, 1.0);
	vec4 view_pos = world_to_view * world_pos;
	vec4 clip_pos = view_to_clip * view_pos;

	gl_Position = clip_pos;
	interpolator_world = world_pos.xyz;
}`;

			var frag_code = `
in vec3 interpolator_world;
out vec4 display;

void main(void) {
	vec3 normal = normalize(cross(dFdx(interpolator_world), dFdy(interpolator_world)));

	display = vec4(normal * 0.5 + 0.5, 1.0);
}`;

			this.gl_shader_program_simple = gl_create_shader_program(gl, vert_code, frag_code);
		}

		// Set unpacking alignement to 1 to allow uploading texture size not multple of 4 
		gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
	}

	release_gl()
	{
		this.canvas = null;
		this.gl = null;
		this.gl_ext = null;
		this.gl_unit_cube_vertex_buffer = null;
		this.gl_unit_cube_index_buffer = null;
		this.gl_shader_program_simple = null;
	}

	draw_canvas()
	{
		console.error('unimplemented');
	}

	init()
	{
		console.error('unimplemented');
	}

	release()
	{
		this.viewport_selected = false;
		this.release_gl();
		this.set_select_viewport(false);
	}

	resize(ctx)
	{
		var dpi = window.devicePixelRatio || 1;
		var viewport_width = ctx.width - 2 * k_style_padding_size - 2;
		var viewport_height = viewport_width * 9.0 / 16.0;

		var canvas_outter = document.getElementById('canvas_outter');

		this.canvas.style.width = `${viewport_width}px`;
		this.canvas.style.height = `${viewport_height}px`;
		this.canvas.width = Math.round(viewport_width * dpi);
		this.canvas.height = Math.round(viewport_height * dpi);
	}

	onclick_canvas()
	{
		if (!this.is_ready)
		{
			return;
		}

		this.set_select_viewport(!this.viewport_selected);
	}

	set_select_viewport(selected)
	{
		if (this.viewport_selected === selected)
		{
			return;
		}

		this.viewport_selected = selected;

		this.canvas.oncontextmenu = function(event) { return false; };

		var canvas_outter = document.getElementById('canvas_outter');

		if (this.viewport_selected)
		{
			var generic_3d_view = this;
			canvas_outter.classList.add('selected');
			//this.canvas.style.cursor = 'none';
			this.canvas.onmousemove = function(event) { generic_3d_view.canvas_onmousemove(event); };
			this.canvas.onwheel = function(event) { return generic_3d_view.canvas_onmousewheel(event); };

			this.keydown_event = function(event) { generic_3d_view.canvas_onkeydown(event); };
			this.keyup_event = function(event) { generic_3d_view.canvas_onkeyup(event); };
			document.addEventListener(
				"keydown",
				this.keydown_event,
				false);
			document.addEventListener(
				"keyup",
				this.keyup_event,
				false);

			this.shedule_next_tick(performance.now());
		}
		else
		{
			canvas_outter.classList.remove('selected');
			this.canvas.style.cursor = 'default';
			this.canvas.onmousemove = null;
			this.canvas.onwheel = null;

			document.removeEventListener(
				"keydown",
				this.keydown_event);
			document.removeEventListener(
				"keyup",
				this.keyup_event);

			this.camera_movement_u = 0;
			this.camera_movement_v = 0;
			this.camera_movement_z = 0;
		}
	}

	canvas_onmousemove(event)
	{
		var dx = event.movementX;
		var dy = event.movementY;

		var speed = (Math.PI / 180.0);

		this.camera_longitude -= dx * speed;
		this.camera_latitude = Math.min(Math.max(this.camera_latitude - dy * speed, -0.4 * Math.PI), 0.4 * Math.PI);
	}

	canvas_onmousewheel(event)
	{
		var zooming_in = event.deltaY < 0.0;

		if (zooming_in)
		{
			this.camera_movement_speed *= 2.0;
		}
		else
		{
			this.camera_movement_speed *= 0.5;
		}

		return false;
	}

	canvas_onkeydown(event)
	{
		if (event.key == 'w')
		{
			this.camera_movement_z = 1.0;
		}
		else if (event.key == 's')
		{
			this.camera_movement_z = -1.0;
		}
		else if (event.key == 'q')
		{
			this.camera_movement_v = 1.0;
		}
		else if (event.key == 'e')
		{
			this.camera_movement_v = -1.0;
		}
		else if (event.key == 'a')
		{
			this.camera_movement_u = -1.0;
		}
		else if (event.key == 'd')
		{
			this.camera_movement_u = 1.0;
		}
	}

	canvas_onkeyup(event)
	{
		if (event.key == 'w' && this.camera_movement_z > 0.0)
		{
			this.camera_movement_z = 0.0;
		}
		else if (event.key == 's' && this.camera_movement_z < 0.0)
		{
			this.camera_movement_z = 0.0;
		}
		else if (event.key == 'q' && this.camera_movement_v > 0.0)
		{
			this.camera_movement_v = 0.0;
		}
		else if (event.key == 'e' && this.camera_movement_v < 0.0)
		{
			this.camera_movement_v = 0.0;
		}
		else if (event.key == 'a' && this.camera_movement_u < 0.0)
		{
			this.camera_movement_u = 0.0;
		}
		else if (event.key == 'd' && this.camera_movement_u > 0.0)
		{
			this.camera_movement_u = 0.0;
		}
	}

	copy_canvas_to_clipboard()
	{
		if (!this.is_ready)
		{
			return;
		}
		
		var pass_data = g_view.pass_data;

		var is_output_resource = is_pass_output_resource(pass_data, this.subresource_version_info);

		var text = '';
		text += `Dump:\n\t${g_infos['Project']} - ${g_infos['Platform']} - ${g_infos['RHI']} - ${g_infos['BuildVersion']} - ${g_infos['DumpTime']}\n\t${get_current_navigation()}\n`
		text += `Pass:\n\t${pass_data['EventName']}\n`;

		{
			var name = prettify_subresource_unique_name(this.subresource_version_info, this.resource_desc);
			text += `${is_output_resource ? 'Output' : 'Input'} resource:\n\t${name}\n`;
		}

		copy_canvas_to_clipboard(this.canvas, text);
	}

	camera_look_at(pos)
	{
		var camera_dir_x = pos[0] - this.camera_pos[0];
		var camera_dir_y = pos[1] - this.camera_pos[1];
		var camera_dir_z = pos[2] - this.camera_pos[2];
		var camera_dir_len = Math.sqrt(camera_dir_x * camera_dir_x + camera_dir_y * camera_dir_y + camera_dir_z * camera_dir_z);

		this.camera_longitude = Math.atan2(camera_dir_y / camera_dir_len, camera_dir_x / camera_dir_len);
		this.camera_latitude = Math.asin(camera_dir_z / camera_dir_len);
	}

	get_view_matrix()
	{
		var camera_dir = [
			Math.cos(this.camera_longitude) * Math.cos(this.camera_latitude),
			Math.sin(this.camera_longitude) * Math.cos(this.camera_latitude),
			Math.sin(this.camera_latitude),
		];

		return create_view_matrix(this.camera_pos, camera_dir);
	}

	get_proj_matrix()
	{
		if (false)
		{
			var width = 4.0;
			var depth = 100.0;

			var matrix = create_null_matrix();
			matrix[0 + 0 * 4] = 1.0 / width;
			matrix[1 + 1 * 4] = matrix[0 + 0 * 4] * this.canvas.width / this.canvas.height;
			matrix[2 + 2 * 4] = - 1.0 / depth;
			matrix[2 + 3 * 4] = 1.0;
			matrix[3 + 3 * 4] = 1.0;
			return matrix;
		}

		return create_projection_matrix_reverse_z_persepective(
			this.camera_fov * 0.5, this.canvas.width / this.canvas.height, /* clipping_plane = */ 10.0);
	}

	shedule_next_tick(current_time)
	{
		var generic_3d_view = this;
		this.prev_time = current_time;
		requestAnimationFrame((new_time) => {
    		generic_3d_view.tick(new_time);
  		});
	}

	tick(current_time)
	{
		if (!this.viewport_selected)
		{
			return;
		}

		var delta_seconds = current_time - this.prev_time;

		{
			var delta_movements = 0.01 * delta_seconds * this.camera_movement_speed;

			var camera_dir = [
				Math.cos(this.camera_longitude) * Math.cos(this.camera_latitude),
				Math.sin(this.camera_longitude) * Math.cos(this.camera_latitude),
				Math.sin(this.camera_latitude),
			];

			var camera_dir_u = [
				Math.sin(this.camera_longitude),
				-Math.cos(this.camera_longitude),
				0.0,
			];

			var camera_dir_v = [
				Math.cos(this.camera_longitude) * Math.sin(-this.camera_latitude),
				Math.sin(this.camera_longitude) * Math.sin(-this.camera_latitude),
				Math.cos(this.camera_latitude),
			];

			this.camera_pos[0] += delta_movements * (
				this.camera_movement_z * camera_dir[0] +
				this.camera_movement_u * camera_dir_u[0] +
				this.camera_movement_v * camera_dir_v[0]);
			this.camera_pos[1] += delta_movements * (
				this.camera_movement_z * camera_dir[1] +
				this.camera_movement_u * camera_dir_u[1] +
				this.camera_movement_v * camera_dir_v[1]);
			this.camera_pos[2] += delta_movements * (
				this.camera_movement_z * camera_dir[2] +
				this.camera_movement_u * camera_dir_u[2] +
				this.camera_movement_v * camera_dir_v[2]);
		}

		this.draw_canvas();
		this.shedule_next_tick(current_time);
	}
} // class Generic3DStructureView


// -------------------------------------------------------------------- HARDWARE ACCELERATION STRUCTURE

class RaytracingAccelerationStructureView extends Generic3DStructureView
{
	constructor(subresource_version_info, resource_desc)
	{
		super(subresource_version_info, resource_desc);
		this.draw_calls = [];
		this.scene_visualization_shader_programs = {};
		this.scene_visualization = 'Instances';
	}

	init()
	{
		document.getElementById('canvas_header').innerHTML += `
			<div class="button_list" id="change_scene_visualization"><!---
				---><input type="button" onclick="g_view.resource_view.change_scene_visualization(this);" value="Instances"/><!--- 
				---><input type="button" onclick="g_view.resource_view.change_scene_visualization(this);" value="Normals"/>
			</div>`;

		update_value_selection(document.getElementById('change_scene_visualization'), this.scene_visualization);

		// Create visualization shader programs
		this.scene_visualization_shader_programs = {};
		{
			var vert_code = `
uniform mat4 local_to_world;
uniform mat4 world_to_view;
uniform mat4 view_to_clip;

in vec3 vertex_coordinates;
out vec3 interpolator_world;

void main(void) {
	vec4 world_pos = local_to_world * vec4(vertex_coordinates.xyz, 1.0);
	vec4 view_pos = world_to_view * world_pos;
	vec4 clip_pos = view_to_clip * view_pos;

	gl_Position = clip_pos;
	interpolator_world = world_pos.xyz;
}`;

			var frag_normals_code = `
in vec3 interpolator_world;
out vec4 display;

void main(void) {
	vec3 normal = normalize(cross(dFdx(interpolator_world), dFdy(interpolator_world)));

	display = vec4(normal * 0.5 + 0.5, 1.0);
}`;

			var frag_instances_code = `
uniform uint instance_index;

in vec3 interpolator_world;
out vec4 display;

uint MurmurMix(uint Hash)
{
	Hash ^= Hash >> 16u;
	Hash *= 0x85ebca6bu;
	Hash ^= Hash >> 13u;
	Hash *= 0xc2b2ae35u;
	Hash ^= Hash >> 16u;
	return Hash;
}

vec3 IntToColor(uint Index)
{
	uint Hash = MurmurMix(Index);

	vec3 Color = vec3
	(
		(Hash >>  0u) & 255u,
		(Hash >>  8u) & 255u,
		(Hash >> 16u) & 255u
	);

	return Color * (1.0f / 255.0f);
}

void main(void) {
	vec3 normal = normalize(cross(dFdx(interpolator_world), dFdy(interpolator_world)));

	vec3 vert_color = IntToColor(1u + instance_index);
	vec3 base_color = vert_color * pow(gl_FragCoord.w, 0.1);

	display = vec4(mix(base_color, normal * 0.5 + 0.5, 1.0 / 3.0), 1.0);
}`;

			this.scene_visualization_shader_programs['Normals'] = gl_create_shader_program(
				this.gl, vert_code, frag_normals_code);
			this.scene_visualization_shader_programs['Instances'] = gl_create_shader_program(
				this.gl, vert_code, frag_instances_code);
		}

		var raytracing_view = this;
		var subresource_version_name = get_subresource_unique_version_name(this.subresource_version_info);

		load_resource_binary_file(`Resources/${subresource_version_name}.bin`, function(raw_raytracing_data)
		{
			if (raw_raytracing_data)
			{
				raytracing_view.parse_raw_raytracing_data(raw_raytracing_data);
				raytracing_view.is_ready = true;
			}
			else
			{
				raytracing_view.error(`Error: Resources/${subresource_version_name}.bin couldn't be loaded.`);
			}
		});

		document.title = `${g_view.pass_data['EventName']} - ${this.resource_desc['Name']}`;
	}

	parse_raw_raytracing_data(raw_raytracing_data)
	{
		var is_little_endian = true;

		var data_view = new DataView(raw_raytracing_data);
		var data_byte_offset = 0;

		function set_byte_offset(byte_offset)
		{
			data_byte_offset = byte_offset;
		}

		function skip_bytes(byte_offset)
		{
			data_byte_offset += byte_offset;
		}

		function read_uint8()
		{
			var r = data_view.getUint8(data_byte_offset);
			data_byte_offset += 1;
			return r;
		}

		function read_uint32()
		{
			var r = data_view.getUint32(data_byte_offset, is_little_endian);
			data_byte_offset += 4;
			return r;
		}

		function read_uint64()
		{
			var l = BigInt(data_view.getUint32(data_byte_offset, is_little_endian));
			var r = BigInt(data_view.getUint32(data_byte_offset + 4, is_little_endian));
			data_byte_offset += 8;
			return is_little_endian
    			? l + BigInt(2 ** 32) * r
    			: BigInt(2 ** 32) * l + r;
		}

		function read_uint32_array(size)
		{
			var r = new Array(size);
			for (var i = 0; i < size; i++)
			{
				r[i] = data_view.getUint32(data_byte_offset, is_little_endian);
				data_byte_offset += 4;
			}
			return r;
		}

		function read_float32_array(size)
		{
			var r = new Array(size);
			for (var i = 0; i < size; i++)
			{
				r[i] = data_view.getFloat32(data_byte_offset, is_little_endian);
				data_byte_offset += 4;
			}
			return r;
		}

		function assert_magic(magic)
		{
			var m = read_uint64();
			if (magic !== m)
			{
				throw new Error(`Magic assertion ${magic} failed`);
			}
		}

		const MaxNumLayers = 64;

		// UE::HWRTScene::FSceneHeader
		assert_magic(0x72ffa3376f48683bn);
		assert_magic(1n);
		var Offsets_Instances = read_uint32();
		var Offsets_Geometries = read_uint32();
		var Offsets_Buffers = read_uint32();
		var Offsets_Strings = read_uint32();
		var NumLayers = read_uint32();
		var PerLayerNumInstances = read_uint32_array(MaxNumLayers);
		var NumInstances = read_uint32();
		var NumGeometries = read_uint32();
		var NumBuffers = read_uint32();
		var NumStrings = read_uint32();
		skip_bytes(4 * 2);

		set_byte_offset(Offsets_Instances);
		var Instances = [];
		for (var i = 0; i < NumInstances; i++)
		{
			var NewInstance = {};
			Instances.push(NewInstance);

			set_byte_offset(Offsets_Instances + i * (3 * 4 * 4 + 4 * 2 + 8));

			// UE::HWRTScene::FInstanceDesc
			NewInstance.Transform = read_float32_array(3 * 4);
			var A = read_uint32();
			var B = read_uint32();
			NewInstance.InstanceID   = (A >>  0) & 0xFFFFFF;
			NewInstance.InstanceMask = (A >> 24) & 0xFF;
			NewInstance.InstanceContributionToHitGroupIndex = (B >>  0) & 0xFFFFFF;
			NewInstance.Flags        = (B >> 24) & 0xFF;
			NewInstance.AccelerationStructure = read_uint64();
		}

		// UE::HWRTScene::FGeometryHeader
		set_byte_offset(Offsets_Geometries);
		var Geometries = [];
		for (var GeometryIndex = 0; GeometryIndex < NumGeometries; GeometryIndex++)
		{
			var Geometry = {};
			Geometries.push(Geometry);

			assert_magic(0x47226e42ad539683n);
			Geometry.IndexBuffer = read_uint32();
			Geometry.NumSegments = read_uint32();
			skip_bytes(4 + 4 + 8);

			// UE::HWRTScene::FSegmentHeader
			Geometry.Segments = [];
			for (var SegmentIndex = 0; SegmentIndex < Geometry.NumSegments; SegmentIndex++)
			{
				var Segment = {};
				Geometry.Segments.push(Segment);

				Segment.VertexBuffer = read_uint32();
				Segment.VertexType = read_uint32();
				Segment.VertexBufferOffset = read_uint32();
				Segment.VertexBufferStride = read_uint32();
				Segment.MaxVertices = read_uint32();
				Segment.FirstPrimitive = read_uint32();
				Segment.NumPrimitives = read_uint32();

				Segment.bForceOpaque = read_uint8();
				Segment.bAllowDuplicateAnyHitShaderInvocation = read_uint8();
				Segment.bEnabled = read_uint8();
				skip_bytes(1);
			}
		}

		// UE::HWRTScene::FBufferHeader
		set_byte_offset(Offsets_Buffers);
		var RawBuffers = [];
		for (var BufferIndex = 0; BufferIndex < NumBuffers; BufferIndex++)
		{
			var RawBuffer = {};
			RawBuffers.push(RawBuffer);

			assert_magic(0x7330d54d0195a6den);
			RawBuffer.SizeInBytes = read_uint32();
			RawBuffer.StrideInBytes = read_uint32();
			RawBuffer.ByteOffset = data_byte_offset;
			skip_bytes(RawBuffer.SizeInBytes);
		}

		// Translate UE::HWRTScene to webgl draw calls
		var webgl_buffers = new Array(RawBuffers.length);
		var min_position = [0, 0, 0];
		var max_position = [0, 0, 0];
		var gl = this.gl;
		for (var instance_index = 0; instance_index < NumInstances; instance_index++)
		{
			var Instance = Instances[instance_index];
			var Geometry = Geometries[Instance.AccelerationStructure];
			var IndexBuffer = RawBuffers[Geometry.IndexBuffer];

			// only support indexed geometry right now
			if (IndexBuffer.SizeInBytes == 0)
			{
				continue;
			}

			// Upload index buffer to GPU if not already.
			if (!webgl_buffers[Geometry.IndexBuffer])
			{
				var indices = null;
				if (IndexBuffer.StrideInBytes == 4)
				{
					indices = new Uint32Array(raw_raytracing_data, IndexBuffer.ByteOffset, IndexBuffer.SizeInBytes / 4);
				}
				else if (IndexBuffer.StrideInBytes == 2)
				{
					indices = new Uint16Array(raw_raytracing_data, IndexBuffer.ByteOffset, IndexBuffer.SizeInBytes / 2);
				}
				else if (IndexBuffer.StrideInBytes == 1)
				{
					indices = new Uint8Array(raw_raytracing_data, IndexBuffer.ByteOffset, IndexBuffer.SizeInBytes / 1);
				}
				else
				{
					throw Error(`Unknown IndexBuffer.StrideInBytes=${IndexBuffer.StrideInBytes}`);
				}
				console.assert(indices.byteOffset == IndexBuffer.ByteOffset);
				console.assert(indices.byteLength == IndexBuffer.SizeInBytes);

				webgl_buffers[Geometry.IndexBuffer] = gl_create_index_buffer(gl, indices);
			}

			// setup local to world
			var local_to_world = create_identity_matrix();
			{
				local_to_world[0 + 0 * 4] = Instance.Transform[0 * 4 + 0];
				local_to_world[1 + 0 * 4] = Instance.Transform[1 * 4 + 0];
				local_to_world[2 + 0 * 4] = Instance.Transform[2 * 4 + 0];
				local_to_world[0 + 1 * 4] = Instance.Transform[0 * 4 + 1];
				local_to_world[1 + 1 * 4] = Instance.Transform[1 * 4 + 1];
				local_to_world[2 + 1 * 4] = Instance.Transform[2 * 4 + 1];
				local_to_world[0 + 2 * 4] = Instance.Transform[0 * 4 + 2];
				local_to_world[1 + 2 * 4] = Instance.Transform[1 * 4 + 2];
				local_to_world[2 + 2 * 4] = Instance.Transform[2 * 4 + 2];
				local_to_world[0 + 3 * 4] = Instance.Transform[0 * 4 + 3];
				local_to_world[1 + 3 * 4] = Instance.Transform[1 * 4 + 3];
				local_to_world[2 + 3 * 4] = Instance.Transform[2 * 4 + 3];
			}

			for (var SegmentIndex = 0; SegmentIndex < Geometry.NumSegments; SegmentIndex++)
			{
				var Segment = Geometry.Segments[SegmentIndex];
				var VertexBuffer = RawBuffers[Segment.VertexBuffer];

				console.assert(Segment.VertexType == 3); // VET_Float3

				// Upload vertex buffer to GPU if not already.
				if (!webgl_buffers[Segment.VertexBuffer])
				{
					var vertices = new Float32Array(raw_raytracing_data, VertexBuffer.ByteOffset, VertexBuffer.SizeInBytes / 4);
					console.assert(vertices.byteOffset == VertexBuffer.ByteOffset);
					console.assert(vertices.byteLength == VertexBuffer.SizeInBytes);
					webgl_buffers[Segment.VertexBuffer] = gl_create_vertex_buffer(gl, vertices);
				}

				var draw_call = {};
				this.draw_calls.push(draw_call);

				draw_call.local_to_world = local_to_world;
				draw_call.instance_index = instance_index;
				if (IndexBuffer.StrideInBytes == 4)
				{
					draw_call.index_type = gl.UNSIGNED_INT;
				}
				else if (IndexBuffer.StrideInBytes == 2)
				{
					draw_call.index_type = gl.UNSIGNED_SHORT;
				}
				else if (IndexBuffer.StrideInBytes == 1)
				{
					draw_call.index_type = gl.UNSIGNED_BYTE;
				}
				else
				{
					throw Error(`Unknown IndexBuffer.StrideInBytes=${IndexBuffer.StrideInBytes}`);
				}
				draw_call.index_byte_offset = Segment.FirstPrimitive * 3 * IndexBuffer.StrideInBytes;
				draw_call.index_count = Segment.NumPrimitives * 3;

				// create vertex array.
				{
					console.assert(Segment.VertexBufferStride == 12);
					console.assert((Segment.VertexBufferOffset % Segment.VertexBufferStride) == 0);

					draw_call.vertex_array = gl.createVertexArray();
					gl.bindVertexArray(draw_call.vertex_array);
					gl.bindBuffer(gl.ARRAY_BUFFER, webgl_buffers[Segment.VertexBuffer]);
					gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, webgl_buffers[Geometry.IndexBuffer]);
					gl.enableVertexAttribArray(0);
					gl.vertexAttribPointer(0, 3, gl.FLOAT, false, Segment.VertexBufferStride, Segment.VertexBufferOffset);
					gl.bindVertexArray(null);
					console.assert(gl.getError() == gl.NO_ERROR); 
				}

				// Track the min and max instance position for seting up the camera over the entire scene.
				if (instance_index == 0)
				{
					min_position[0] = Instance.Transform[0 * 4 + 3];
					min_position[1] = Instance.Transform[1 * 4 + 3];
					min_position[2] = Instance.Transform[2 * 4 + 3];

					max_position[0] = Instance.Transform[0 * 4 + 3];
					max_position[1] = Instance.Transform[1 * 4 + 3];
					max_position[2] = Instance.Transform[2 * 4 + 3];
				}
				else
				{
					min_position[0] = Math.min(min_position[0], Instance.Transform[0 * 4 + 3]);
					min_position[1] = Math.min(min_position[0], Instance.Transform[1 * 4 + 3]);
					min_position[2] = Math.min(min_position[0], Instance.Transform[2 * 4 + 3]);

					max_position[0] = Math.max(max_position[0], Instance.Transform[0 * 4 + 3]);
					max_position[1] = Math.max(max_position[0], Instance.Transform[1 * 4 + 3]);
					max_position[2] = Math.max(max_position[0], Instance.Transform[2 * 4 + 3]);
				}
			}
		}

		if (true)
		{
			this.camera_pos = max_position;
			this.camera_look_at([(min_position[0] + max_position[0]) / 2, (min_position[1] + max_position[1]) / 2, (min_position[2] + max_position[2]) / 2]);
		}

		this.draw_canvas();
	} // parse_raw_raytracing_data()

	change_scene_visualization(new_scene_visualization)
	{
		if (!this.is_ready)
		{
			return;
		}
		
		if (new_scene_visualization.value == this.scene_visualization)
		{
			return;
		}

		this.scene_visualization = new_scene_visualization.value;
		update_value_selection(document.getElementById('change_scene_visualization'), this.scene_visualization);
		this.draw_canvas();
	}

	draw_canvas()
	{
		var gl = this.gl;

		var world_to_view = this.get_view_matrix();
		var view_to_clip = this.get_proj_matrix();

		// view_matrix[0 + 0 * 4] = 0.5;
		// view_matrix[0 + 1 * 4] = 0.5;
		// view_matrix[1 + 1 * 4] = 0.5;
		// view_matrix[2 + 2 * 4] = 0.5;

		gl.viewport(0, 0, this.canvas.width, this.canvas.height);
		gl.clearColor(0.0, 0.0, 0.0, 0.0);
		gl.clear(gl.COLOR_BUFFER_BIT);
		
		gl.clearDepth(0.0);
		gl.clear(gl.DEPTH_BUFFER_BIT);

		gl.enable(gl.DEPTH_TEST);
		gl.depthFunc(gl.GEQUAL);

		gl.disable(gl.CULL_FACE);
		gl.cullFace(gl.FRONT_AND_BACK);

		{
			var shader_program = this.scene_visualization_shader_programs[this.scene_visualization];
			gl.useProgram(shader_program);

			var coord = gl.getAttribLocation(shader_program, "vertex_coordinates");
			console.assert(coord == 0);

			gl_set_uniform_mat4(gl, shader_program, 'local_to_world', create_identity_matrix());
			gl_set_uniform_mat4(gl, shader_program, 'world_to_view', world_to_view);
			gl_set_uniform_mat4(gl, shader_program, 'view_to_clip', view_to_clip);

			for (var draw_call of this.draw_calls)
			{
				gl_set_uniform_mat4(gl, shader_program, 'local_to_world', draw_call.local_to_world);
				gl_set_uniform_uint(gl, shader_program, 'instance_index', draw_call.instance_index);

				gl.bindVertexArray(draw_call.vertex_array);
				gl.drawElementsInstanced(
					gl.TRIANGLES,
					/* index_count = */       draw_call.index_count,
					/* index_type = */        draw_call.index_type,
					/* index_byte_offset = */ draw_call.index_byte_offset,
					/* instanceCount = */     1);
			}

			gl.useProgram(null);
			gl.bindVertexArray(null);
		}
	}

	release()
	{
		super.release();
		this.draw_calls = []
		this.scene_visualization_shader_programs = {};
	}
} // class RaytracingAccelerationStructureView


// -------------------------------------------------------------------- UI

function update_href_selection(parent)
{
	var all_a = parent.getElementsByTagName("a");

	var navs = [location.hash.substring(1)];
	if (g_view)
	{
		navs = navs.concat(g_view.navigations);
	}

	for (let a of all_a)
	{
		var href = a.href.split('#');

		if (navs.includes(`${href[1]}`))
		{
			a.classList.add('match_location_hash');
		}
		else if (a.classList.contains('match_location_hash'))
		{
			a.classList.remove('match_location_hash');
		}
	};
}

function update_value_selection(parent, selected_value)
{
	var all_a = parent.getElementsByTagName("input");

	for (let a of all_a)
	{
		if (a.value == selected_value)
		{
			a.classList.add('match_value');
		}
		else if (a.classList.contains('match_value'))
		{
			a.classList.remove('match_value');
		}
	};
}

function render_selection_list_html(parent_dom, display_list, options)
{
	if ('deduplicate' in options)
	{
		var href_set = new Set();
		var new_display_list = [];
		for (const display_list_item of display_list)
		{
			if (display_list_item['href'])
			{
				if (!href_set.has(display_list_item['href']))
				{
					href_set.add(display_list_item['href']);
					new_display_list.push(display_list_item);
				}
			}
			else
			{
				new_display_list.push(display_list_item);
			}
		}
		display_list = new_display_list;
	}

	if ('sort' in options)
	{
		display_list.sort(function(a, b)
		{
			if (a['name'] < b['name'])
			{
				return -1;
			}
			else if (a['name'] > b['name'])
			{
				return 1;
			}
			return 0;
		});
	}

	var search = '';
	if ('search' in options)
	{
		search = options['search'];
	}

	var html = '';
	for (const display_list_item of display_list)
	{
		if (search && !display_list_item['name'].toLowerCase().includes(search.toLowerCase()))
		{
			continue;
		}

		if (display_list_item['href'])
		{
			html += `<a href="${display_list_item['href']}">${display_list_item['name']}</a>`;
		}
		else
		{
			html += `<a>${display_list_item['name']}</a>`;
		}
	}

	if (html == '')
	{
		if (search)
		{
			html += `<div class="empty">No matches for "${search}"</div>`;
		}
		else
		{
			html += `<div class="empty">None</div>`;
		}
	}

	parent_dom.innerHTML = html;
	update_href_selection(parent_dom);
}


// -------------------------------------------------------------------- WINDOW LAYOUT

function init_top()
{
	document.getElementById('top_bar').innerHTML = `${g_infos['Project']} - ${g_infos['Platform']} - ${g_infos['RHI']} - ${g_infos['BuildVersion']} - ${g_infos['DumpTime']} `;
}

function init_top_buttons()
{
	document.getElementById('top_buttons').innerHTML = `
		<a href="#display_infos();">Infos</a>
		<a href="#display_cvars();">CVars</a>
		<a href="#display_log();">Log</a>`;
	document.getElementById('top_viewer_buttons').innerHTML = `
		<a id="console_button" href="#display_console();">Console</a>`;
	update_console_button();
}

function init_page()
{
	// Load the dump service information to know how to read files. This is independent of Base/Infos.json because may depends whether it was uploaded through the DumpGPUServices plugin.
	g_dump_service = load_json('Base/DumpService.json');

	// Load the base information of the page.
	g_infos = load_json('Base/Infos.json');

	if (!g_dump_service || !g_infos)
	{
		document.body.innerHTML = 'Please use OpenGPUDumpViewer script for the viewer to work correctly.';
		return;
	}

	// Verify the status of the dump, to check whether a crash happened during the dump.
	{
		var dump_status = load_text_file('Base/Status.txt');
		if (dump_status == 'ok')
		{
			add_console_event('log', `The dump completed gracefully.`);
		}
		else if (dump_status == 'crash')
		{
			add_console_event('error', `A crash happened while the frame was being dumped.`);
		}
		else if (dump_status == 'dumping')
		{
			add_console_event('error', `The dump has not completed. This may be due to opening the viewer before completion, or serious problem that has not been handled with FCoreDelegates::OnShutdownAfterError`);
		}
		else
		{
			add_console_event('error', `The dump completed with status: ${dump_status}`);
		}
	}

	// Load the cvars used for dumping.
	{
		g_dump_cvars = {};

		var cvars_csv = load_text_file('Base/ConsoleVariables.csv');
		var csv_lines = cvars_csv.split('\n');
		for (var i = 1; i < csv_lines.length - 1; i++)
		{
			var csv_line = csv_lines[i].split(',');
			var entry = {};
			entry['name'] = csv_line[0];
			if (entry['name'].startsWith('r.DumpGPU.'))
			{
				entry['type'] = csv_line[1];
				entry['set_by'] = csv_line[2];
				entry['value'] = csv_line[3];
				g_dump_cvars[entry['name']] = entry['value'];
			}
		}

	}

	// Load all the passes.
	g_passes = load_json_dict_sequence('Base/Passes.json');
	{
		for (var pass_id = 0; pass_id < g_passes.length; pass_id++)
		{
			g_passes[pass_id]['DrawCount'] = 0;
			if (!g_passes[pass_id]['EventName'])
			{
				g_passes[pass_id]['EventName'] = 'Unnamed pass';
			}
		}

		var pass_draw_counts = load_json_dict_sequence('Base/PassDrawCounts.json');
		if (pass_draw_counts)
		{
			pass_draw_counts.forEach(function(pass_draw_count_data) {
				for (var pass_id = 0; pass_id < g_passes.length; pass_id++)
				{
					if (g_passes[pass_id]['Pointer'] == pass_draw_count_data['Pointer'])
					{
						g_passes[pass_id]['DrawCount'] = pass_draw_count_data['DrawCount'];
						break;
					}
				}
			});
		}
	}

	// Load all the resource descriptors
	{
		g_descs = {};

		var desc_list = load_json_dict_sequence('Base/ResourceDescs.json');
		for (const desc of desc_list)
		{
			g_descs[desc['UniqueResourceName']] = desc;
		}
	}

	if (!does_file_exists(get_log_path()))
	{
		add_console_event('error', `The dump does not contain ${get_log_path()}. The log is normally copied into the dump directory once the dump is completed. Failing to have is may be due to a premature end of the dumping process.`);
	}
	else
	{
		add_console_event('log', `Viewer init ok`);
	}

	// Analyses the capture.
	{
		analyses_passes();
	}

	init_top();
	init_top_buttons();
	display_pass_hierarchy();

	// Find the last passes that have a displayable texture 2D.
	if (location.hash)
	{
		return navigate_to_hash();
	}
	
	// Find the last pass that modify the RDG resource set by 'r.DumpGPU.Viewer.Visualize'
	if ('r.DumpGPU.Viewer.Visualize' in g_dump_cvars && g_dump_cvars['r.DumpGPU.Viewer.Visualize'] != '')
	{
		var pass_id_to_open = -1;
		var subresource_unique_name_to_open = null;

		document.getElementById('resource_search_input').value = g_dump_cvars['r.DumpGPU.Viewer.Visualize'];
		display_pass_hierarchy();

		for (var pass_id = 0; pass_id < g_passes.length && pass_id_to_open == -1; pass_id++)
		{
			var pass_data = g_passes[g_passes.length - 1 - pass_id];

			for (var subresource_unique_name of pass_data['OutputResources'])
			{
				var subresource_info = parse_subresource_unique_name(subresource_unique_name);
				var resource_desc = get_resource_desc(subresource_info['resource']);

				if (resource_desc === null)
				{
					continue;
				}

				if (resource_desc['Name'] == g_dump_cvars['r.DumpGPU.Viewer.Visualize'] && pass_id_to_open == -1)
				{
					pass_id_to_open = g_passes.length - 1 - pass_id;
					subresource_unique_name_to_open = subresource_unique_name;
					break;
				}
			}
		}

		if (pass_id_to_open != -1)
		{
			return redirect_to_hash(`display_output_resource(${pass_id_to_open},'${subresource_unique_name_to_open}');`);
		}
	}
	
	return display_tip();
}

function onresize_body()
{
	var window_width = window.innerWidth;
	var window_height = window.innerHeight;

	const top_h = 45;

	document.getElementById('onresize_body_table').style.width = `${window_width}px`;
	document.getElementById('onresize_body_table').style.height = `${window_height}px`;

	const top_height = 45;

	var body_left_width = Math.floor(window_width * 0.25);
	var body_right_width = window_width - body_left_width;
	var body_height = window_height - top_height;
	{
		document.getElementById('onresize_body_top_bar').style.width = `${window_width}px`;
		document.getElementById('onresize_body_top_bar').style.height = `${top_height}px`;
		document.getElementById('onresize_body_top_bar').style.overflow = `hidden`;

		document.getElementById('onresize_body_left').style.width = `${body_left_width}px`;
		document.getElementById('onresize_body_left').style.height = `${body_height}px`;

		document.getElementById('onresize_body_right').style.width = `${body_right_width}px`;
		document.getElementById('onresize_body_right').style.height = `${body_height}px`;
	}

	{
		const search_h = (30 + 2 * 5) * 2;

		document.getElementById('pass_hierarchy').style.width = `${body_left_width - k_style_padding_size * 2}px`;
		document.getElementById('pass_hierarchy').style.height = `${body_height - search_h - k_style_padding_size * 2 - k_style_scroll_width}px`;
		document.getElementById('pass_hierarchy').style.overflow = `scroll`;
	}

	{
		document.getElementById('main_right_pannel').style.width = `${body_right_width}px`;
		document.getElementById('main_right_pannel').style.height = `${body_height}px`;
		document.getElementById('main_right_pannel').style.overflow = `scroll`;
	}

	if (g_view !== null)
	{
		var ctx = {};
		ctx.width = body_right_width - k_style_scroll_width;
		ctx.height = body_height - k_style_scroll_width;
		g_view.resize(ctx);
	}
}


// -------------------------------------------------------------------- NAVIGATION

var g_previous_location_hash = null;

function redirect_to_hash(new_hash)
{
	if (new_hash === g_previous_location_hash)
	{
		return;
	}

	location.hash = new_hash;
}

function navigate_to_hash()
{
	if (location.hash === g_previous_location_hash)
	{
		return;
	}

	g_previous_location_hash = location.hash;

	if (location.hash)
	{
		var js = location.hash.substring(1);
		eval(js);
	}
	else
	{
		display_tip();
	}

	update_href_selection(document);
}

function get_current_navigation()
{
	return g_previous_location_hash;
}

</script>


<style type="text/css">


/* ----------------------------------------------------- them colors */

:root
{
	--border-radius: 2px;

	--body-bg-color: rgb(21, 21, 21);
	--body-txt-size: 12px;

	--div-bg-color: rgb(36, 36, 36);
	--div-txt-color: rgb(192, 192, 192);
	--a-bg-color-odd: rgb(26, 26, 26);
	--a-bg-color-hover: #332222;
	--a-bg-color-hoverhref: rgb(38, 67, 81);
	--a-txt-color-disabled: rgb(113, 113, 113);

	--a-bg-color-selected: rgb(38, 187, 255);
	--a-txt-color-selected: rgb(15, 15, 15);

	--channel-red-color: rgb(255, 38, 38);
	--channel-green-color: rgb(38, 255, 38);
	--channel-blue-color: rgb(38, 187, 255);
	--channel-alpha-color: white;

	--error-txt-color: var(--channel-red-color);

	--header-bg-color: rgb(47, 47, 47);

	--button-border-radius: var(--border-radius);
	--button-bg-color: rgb(56, 56, 56);
	--button-bg-color-hover: rgb(87, 87, 87);
	--button-bg-color-selected: rgb(15, 15, 15);
	--button-txt-color: var(--div-txt-color);
	--button-txt-color-hover: rgb(255, 255, 255);
	--button-txt-color-selected: rgb(38, 187, 255);


	--scroll-bg-color: var(--a-bg-color-odd);
	--scroll-color: rgb(87, 87, 87);
	--scroll-color-hover: rgb(128, 128, 128);

	--input-bg-color: rgb(15, 15, 15);
	--input-border: 1px solid rgb(55, 55, 55);
	--input-border-hover: rgb(83, 83, 83);
	--input-border-focus: rgb(38, 176, 239);
}


/* ----------------------------------------------------- override default */

body
{
	overflow: hidden;
	background: var(--body-bg-color);
	color: var(--div-txt-color);
	margin: 0;
	padding: 0;
	/*font-family: Arial, Helvetica, sans-serif;*/
	font-family: consolas, "Liberation Mono", courier, monospace;
	font-size: var(--body-txt-size);
	/* overflow: hidden; */
}

table, tr, td, div, a
{
	margin: 0;
	padding: 0;
	border-spacing: 0;
	border-collapse: collapse;
	vertical-align: top;
	cursor: default;
	font-size: inherit;
}

td
{
	cursor: default;
}

div, a
{
	display: block;
	cursor: default;
}

a, a:hover, a:visited
{
	color: inherit;
	font: inherit;
	text-decoration: none;
	cursor: default;
}


/* ----------------------------------------------------- external link */

a.external_link
{
	color: inherit;
	cursor: pointer;
}

a.external_link:hover
{
	color: var(--button-txt-color-hover);
}

a.external_link:active
{
	color: var(--button-txt-color-selected);
}


/* ----------------------------------------------------- inputs scroll bars */

input
{
	padding: 3px 5px;
	border-radius: var(--border-radius);
}

input[type=search]
{
	padding: 3px 13px;
	border-radius: 20px;
}

textarea
{
	padding: 3px 3px;
}

input, textarea
{
	background-color: var(--input-bg-color);
	color: var(--div-txt-color);
	border: var(--input-border);
	outline:  none;
	font-size: inherit;
	font: inherit;
}

input[readonly], textarea[readonly]
{
	color: var(--a-txt-color-disabled);
}

input:not([readonly]):hover,
textarea:not([readonly]):hover
{
	border-color: var(--input-border-hover);
}

input[type=text]:not([readonly]):focus,
input[type=search]:not([readonly]):focus,
textarea:not([readonly]):focus
{
	color: rgb(254, 254, 254);
	border-color: var(--input-border-focus);
}

input:focus, textarea:focus
{
	outline: none;
}

input::placeholder
{
	color: rgb(76, 76, 76);
}


/* ----------------------------------------------------- webkit scroll bars */

*::-webkit-scrollbar {
	width: 8px;
	height: 8px;
	background: var(--scroll-bg-color);
}

*::-webkit-scrollbar-corner, *::-webkit-scrollbar-track {
	background: var(--scroll-bg-color);
}

*::-webkit-scrollbar-thumb {
	background-color: var(--scroll-color);
	border-radius: 20px;
}

*::-webkit-scrollbar-thumb:hover {
	background-color: var(--scroll-color-hover);
	border-radius: 20px;
}


/* ----------------------------------------------------- common layout */

.main_div
{
	background: var(--div-bg-color);
	padding: 5px;
	margin: 5px;
	border-radius: 3px;
}

#main_right_pannel .main_div
{
	margin-bottom: 20px;
}

#main_right_pannel .main_div:last-child
{
	margin-bottom: 5px;
}


/* ----------------------------------------------------- Selection list */

.selection_list_title
{
	font-size: 20;
	font-weight: bold;
	padding: 5px 20px 10px 20px;
}

.selection_list_search
{
	padding: 0px 5px;
	height: 30px;
	width: auto;
}

.selection_list_search input[type=search]
{
	width: 100%;
}

.selection_list
{
	max-height: inherit;
	overflow-x: auto;
	overflow-y: scroll;
}

.selection_list a
{
	width: auto;
	padding: 5px 20px;
	white-space: nowrap;
}

.selection_list a:nth-child(odd)
{
	background: var(--a-bg-color-odd);
}

.selection_list a:not(.match_location_hash):hover
{
	background: var(--a-bg-color-hover);
}

.selection_list a:not(.match_location_hash)[href]:hover
{
	background: var(--a-bg-color-hoverhref);
	color: var(--button-txt-color-hover);
	cursor: pointer;
}

/*.selection_list a.match_location_hash
{
	background: var(--a-bg-color-selected);
	color: var(--a-txt-color-selected);
}*/

.selection_list a.match_location_hash
{
	background: var(--button-bg-color-selected);
	color: var(--button-txt-color-selected);
}

.selection_list a.disabled
{
	color: var(--a-txt-color-disabled);
}

.selection_list div.empty
{
	width: 100%;
	margin: 20px 0 0 0;
	color: var(--a-txt-color-disabled);
	text-align: center;
}

#main_right_pannel .selection_list
{
	min-height: 100px;
	max-height: 200px;
}


/* ----------------------------------------------------- table */

.pretty_table tr.header
{
	background: var(--header-bg-color);
	font-weight: bold;
}

.pretty_table tr:not(.header):nth-child(odd) td
{
	background: var(--a-bg-color-odd);
}

.pretty_table tr:not(.header):hover td:not(.empty)
{
	background: var(--a-bg-color-hover);
}

.pretty_table tr td
{
	display: table-cell;
	padding: 5px 20px;
}

.pretty_table tr td:first-child
{
	width: 150px;
}

.pretty_table tr.header td
{
	padding: 5px 30px;
}

.pretty_table tr.header td:not(:first-child)
{
	border-left: 1px solid rgb(36, 36, 36);
}

.pretty_table .empty
{
	padding: 20px 0 0 0;
	color: var(--a-txt-color-disabled);
	text-align: center;
}

.pretty_table tr.error td
{
	color: var(--error-txt-color);
}

.resource_desc tr td:not(.empty)
{
	width: 150px;
	text-align: right;
}


/* ----------------------------------------------------- button */

.button_list a,
.button_list input
{
	margin: 0;
	display: inline-block;
	display: table-cell;
	padding: 3px 10px;
	background-color: var(--button-bg-color);
	color: var(--button-txt-color);
	border-radius: 0;
}

.button_list a[href],
.button_list input
{
	cursor: pointer;
}

.button_list a:first-child,
.button_list input:first-child
{
	border-top-left-radius: var(--button-border-radius);
	border-bottom-left-radius: var(--button-border-radius);
}

.button_list a:last-child,
.button_list input:last-child
{
	border-top-right-radius: var(--button-border-radius);
	border-bottom-right-radius: var(--button-border-radius);
}

.button_list a[href]:not(.match_location_hash):not(:active):hover,
.button_list input:not(.match_value):not(:active):hover
{
	background-color: var(--button-bg-color-hover);
}

.button_list a[href]:not(.match_location_hash):not(.error):not(:active):hover,
.button_list input:not(.match_value):not(:active):hover
{
	color: var(--button-txt-color-hover);
}

.button_list a[href]:active,
.button_list a[href].match_location_hash,
.button_list input:active,
.button_list input.match_value
{
	background-color: var(--button-bg-color-selected);
	color: var(--button-txt-color-selected);
}

.button_list a[href].error,
.button_list a[href].error:active
{
	color: var(--error-txt-color);
}

.button_list span
{
	margin-left: 5px;
	margin-right: 3px;
}


/* ----------------------------------------------------- top bar */

#top_bar
{
	font-size: 20;
	font-weight: bold;
	padding: 10px;
	display: inline-block;
}

#top_buttons,
#top_viewer_buttons
{
	padding: 12px;
	display: inline-block;
}


/* ----------------------------------------------------- main_right_pannel */

#main_right_pannel .pass_title
{
	font-size: 20;
	font-weight: bolder;
	padding: 10px 40px;
}

#main_right_pannel .pass_title .button_list
{
	font-size: var(--body-txt-size);
	margin-top: 3px;
}

#cvars_pannel
{
	height: auto;
	max-height: none;
}

#cvars_pannel .pretty_table tr td:nth-child(2)
{
	text-align: right;
}


/* ----------------------------------------------------- TextureView */

#canvas_outter
{
	background-color: var(--input-bg-color);
	color: var(--div-txt-color);
	border: var(--input-border);
}

#canvas_outter:not(.selected):hover
{
	border-color: var(--input-border-hover);
}

#canvas_outter.selected
{
	border-color: var(--input-border-focus);
}

#canvas_outter #canvas_header
{
	padding: 3px;
	border-bottom: var(--input-border);
}

#canvas_outter #canvas_header .button_list
{
	display: inline-block;
}

#canvas_outter #canvas_footer
{
	padding: 3px 10px;
	border-top: var(--input-border);
}

#canvas_outter #canvas_viewport
{
	overflow: hidden;
    background-image:
		linear-gradient(45deg, #000 25%, transparent 25%),
		linear-gradient(45deg, transparent 75%, #000 75%),
		linear-gradient(45deg, transparent 75%, #000 75%),
		linear-gradient(45deg, #000 25%, var(--input-bg-color) 25%);
    background-size:16px 16px;
    background-position:0 0, 0 0, -8px -8px, 8px 8px;
}

#canvas_outter .error_msg
{
	width: 100%;
	height: 100%;
	text-align: center;
	color: var(--error-txt-color);
}

#canvas_outter canvas
{
	cursor: crosshair;
	image-rendering: optimizeSpeed;
	image-rendering: -moz-crisp-edges;
	image-rendering: -webkit-optimize-contrast;
	image-rendering: -o-crisp-edges;
	image-rendering: pixelated;
	-ms-interpolation-mode: nearest-neighbor;
}

#texture_visualization_code_input
{
	min-width: 800px;
	min-height: 200px;
	display: block;
	width: auto;
}

#texture_visualization_code_log
{
	margin-top: 10px;
	min-width: 800px;
	min-height: 200px;
	display: block;
	width: auto;
}


/* ----------------------------------------------------- BufferView */

#buffer_outter
{
	background-color: var(--input-bg-color);
	color: var(--div-txt-color);
	border: var(--input-border);
}

#buffer_outter:not(.selected):hover
{
	border-color: var(--input-border-hover);
}

#buffer_outter.selected
{
	border-color: var(--input-border-focus);
}

#buffer_outter #buffer_viewport_header
{
	padding: 3px;
	border-bottom: var(--input-border);
}

#buffer_outter #buffer_viewport
{
	overflow: hidden;
}

#buffer_outter #buffer_viewport #buffer_content_table tr.header
{
	position: sticky;
	top: 0;
}

#buffer_outter #buffer_viewport #buffer_content_table:not(.display_elem_using_rows) tr.header td:first-child
{
	padding: 0 3px;
}

#buffer_outter #buffer_viewport #buffer_content_table tr td:not(.empty)
{
	padding: 3px 5px 0px 5px;
	width: 60px;
	height: 20px;
	overflow: hidden;
	text-align: right;
	/*font-family: consolas, "Liberation Mono", courier, monospace;*/
}

#buffer_outter #buffer_viewport #buffer_content_table.display_elem_using_rows tr td:not(.empty):first-child
{
	text-align: left;
}

#buffer_outter #buffer_viewport #buffer_content_table tr td:first-child
{
	width: 100px;
}

#buffer_outter #buffer_viewport #buffer_content_table tr td:last-child
{
	width: auto;
}

#buffer_outter #buffer_viewport #buffer_content_table tr.highlighted
{
	color: var(--button-txt-color-selected);
}

</style>


<body onload="init_console(); onresize_body(); init_page();" onresize="onresize_body();" onhashchange="navigate_to_hash();">
	<table cellpadding="0" cellspacing="0" border="0" id="onresize_body_table">
		<tr>
			<td colspan="2" id="onresize_body_top_bar">
				<div id="top_bar"></div>
				<div id="top_buttons" class="button_list"></div>
				<div id="top_viewer_buttons" class="button_list"></div>
			</td>
		</tr>
		<tr>
			<td valign="top" id="onresize_body_left">
				<div class="main_div">
					<div id="pass_hierarchy_search" style="padding: 5px; height: 70px;">
						<input type="search" id="pass_search_input" oninput="display_pass_hierarchy();" onchange="display_pass_hierarchy();" style="width: 100%;" placeholder="Search pass..." />
						<input type="search" id="resource_search_input" oninput="display_pass_hierarchy();" onchange="display_pass_hierarchy();" style="width: 100%; margin-top: 10px;" placeholder="Search resource..." />
					</div>
					<div id="pass_hierarchy" class="selection_list"></div>
				</div>
			</td>
			<td valign="top" id="onresize_body_right">
				<div id="main_right_pannel"></div>
			</td>
		</tr>
	</table>
</body>
</html>
